web-app/docs/03-core-services/index.md
padreug 71cec00bfc Add Wallet Module documentation and WebSocket integration
- Introduced comprehensive documentation for the new Wallet Module, detailing its purpose, features, and key components.
- Added WebSocket integration documentation, outlining real-time balance updates, connection management, and error handling.
- Updated README and module index files to include references to the Wallet Module, enhancing overall module visibility and accessibility.

These changes improve the clarity and usability of the Wallet Module, providing developers with essential information for implementation and integration.
2025-09-18 09:56:19 +02:00

20 KiB

⚙️ Core Services Overview

Shared infrastructure services providing foundational functionality across all modules with reactive architecture, dependency injection, and lifecycle management.

Table of Contents

Service Architecture

BaseService Foundation

All core services extend the BaseService abstract class which provides:

  • Reactive State Management - Integration with Vue's reactivity system
  • Lifecycle Management - Standardized initialization and disposal
  • Error Handling - Consistent error patterns across services
  • Type Safety - Full TypeScript support with strict typing
abstract class BaseService {
  protected isInitialized = ref(false)
  protected isDisposed = ref(false)
  
  abstract initialize(): Promise<void>
  abstract dispose(): Promise<void>
  
  // Reactive state helpers
  protected createReactiveState<T>(initialValue: T): Ref<T>
  protected createComputedState<T>(getter: () => T): ComputedRef<T>
}

Service Registration Pattern

Services are registered in the dependency injection container during module installation:

// Service registration (in base module)
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
container.provide(SERVICE_TOKENS.AUTH_SERVICE, authService)
container.provide(SERVICE_TOKENS.STORAGE_SERVICE, storageService)

// Service consumption (anywhere in app)
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
const auth = injectService(SERVICE_TOKENS.AUTH_SERVICE)

Available Services

AuthService 🔐

Purpose: User authentication and identity management
Location: src/modules/base/auth/auth-service.ts
Token: SERVICE_TOKENS.AUTH_SERVICE

Key Features:

  • Key Management - Secure generation, import, and storage of Nostr keys
  • User Sessions - Persistent authentication with encrypted storage
  • Profile Management - User profile creation and updates
  • Security - Client-side key handling with no server storage

Reactive State:

interface AuthService {
  user: Ref<NostrUser | null>                    // Current authenticated user
  isAuthenticated: ComputedRef<boolean>          // Authentication status
  isLoading: Ref<boolean>                        // Loading state
  loginError: Ref<string | null>                 // Login error message
}

Key Methods:

  • generateKeyPair() - Create new Nostr key pair
  • loginWithPrivateKey(privateKey: string) - Authenticate with existing key
  • logout() - Clear session and user data
  • updateProfile(profile: UserMetadata) - Update user profile

See: authentication

PaymentService 💳

Purpose: Centralized Lightning payment processing and wallet management Location: src/core/services/PaymentService.ts Token: SERVICE_TOKENS.PAYMENT_SERVICE

Key Features:

  • Wallet Management - Multi-wallet support with preferred wallet selection
  • Payment Processing - Lightning invoices, QR code generation, and external wallet handling
  • Balance Management - Real-time wallet balance updates and tracking
  • Payment Validation - Balance checking and payment amount validation
  • Error Handling - Consistent payment error handling and user feedback

Reactive State:

interface PaymentService {
  isProcessingPayment: ComputedRef<boolean>       // Payment in progress
  paymentError: ComputedRef<string | null>        // Payment error state
  totalBalance: number                            // Total balance across wallets (getter)
  hasWalletWithBalance: boolean                   // Wallet availability check (getter)
}

Key Methods:

  • payWithWallet(paymentRequest, amount?, options?) - Process Lightning payment
  • generateQRCode(paymentRequest, options?) - Generate payment QR codes
  • openExternalWallet(paymentRequest) - Launch external Lightning wallet
  • updateWalletBalance(balanceMsat, walletId?) - Update wallet balance from WebSocket
  • getPreferredWallet() - Get the primary wallet for operations
  • getWalletWithBalance(requiredAmount?) - Find wallet with sufficient balance

Wallet Selection Logic:

  • Always uses first wallet (wallets[0]) for consistency across modules
  • Provides helper methods for wallet selection and balance checking
  • Integrates with AuthService for wallet credentials and management

See: payment-service

RelayHub 🌐

Purpose: Centralized Nostr relay connection management
Location: src/modules/base/nostr/relay-hub.ts
Token: SERVICE_TOKENS.RELAY_HUB

Key Features:

  • Connection Management - Automatic connection, reconnection, and failover
  • Event Publishing - Reliable event publishing across multiple relays
  • Subscription Management - Efficient event subscriptions with deduplication
  • Performance Monitoring - Relay latency and success rate tracking

Reactive State:

interface RelayHub {
  connectedRelays: Ref<string[]>                 // Currently connected relays
  connectionStatus: ComputedRef<ConnectionStatus> // Overall connection status
  relayStats: Ref<Map<string, RelayStats>>       // Per-relay statistics
  isConnecting: Ref<boolean>                     // Connection in progress
}

Key Methods:

  • connect(relays: string[]) - Connect to relay URLs
  • publishEvent(event: NostrEvent) - Publish event to all connected relays
  • subscribe(filters: Filter[], callback: EventCallback) - Subscribe to events
  • getRelayInfo(url: string) - Get relay connection information

See: ../01-architecture/relay-hub

StorageService 💾

Purpose: User-scoped local storage operations
Location: src/core/services/StorageService.ts
Token: SERVICE_TOKENS.STORAGE_SERVICE

Key Features:

  • User-Scoped Storage - Automatic key prefixing per authenticated user
  • Type-Safe Operations - Strongly typed get/set operations with JSON serialization
  • Reactive Updates - Optional reactive storage with Vue refs
  • Migration Support - Data migration between storage schema versions

Key Methods:

interface StorageService {
  setUserData<T>(key: string, data: T): void
  getUserData<T>(key: string, defaultValue?: T): T | undefined
  removeUserData(key: string): void
  clearUserData(): void
  
  // Reactive variants
  getReactiveUserData<T>(key: string, defaultValue: T): Ref<T>
  setReactiveUserData<T>(key: string, ref: Ref<T>): void
}

Storage Patterns:

  • User-specific keys: user:{pubkey}:settings
  • Global application keys: app:theme
  • Module-specific keys: user:{pubkey}:chat:contacts

See: storage-service

ToastService 📢

Purpose: Application-wide notifications and user feedback
Location: src/core/services/ToastService.ts
Token: SERVICE_TOKENS.TOAST_SERVICE

Key Features:

  • Context-Specific Methods - Pre-configured toasts for common scenarios
  • Consistent Messaging - Standardized success, error, and info messages
  • Accessibility - Screen reader compatible notifications
  • Customizable - Support for custom toast content and actions

Organized by Context:

interface ToastService {
  // Authentication context
  auth: {
    loginSuccess(): void
    loginError(error?: string): void
    logoutSuccess(): void
    keyGenerated(): void
  }
  
  // Payment context
  payment: {
    invoiceCreated(): void
    paymentReceived(): void
    paymentFailed(error?: string): void
  }
  
  // Clipboard operations
  clipboard: {
    copied(item?: string): void
    copyFailed(): void
  }
  
  // General operations
  operation: {
    success(message: string): void
    error(message: string): void
    info(message: string): void
  }
}

See: toast-service

EventBus 📡

Purpose: Inter-module communication and event coordination
Location: src/core/services/EventBus.ts
Token: SERVICE_TOKENS.EVENT_BUS

Key Features:

  • Type-Safe Events - Strongly typed event payloads
  • Module Isolation - Clean communication between modules
  • Event Namespacing - Organized event names by domain
  • Subscription Management - Easy subscribe/unsubscribe patterns

Event Categories:

interface EventBusEvents {
  // User events
  'user:authenticated': { userId: string, profile: UserMetadata }
  'user:profile-updated': { userId: string, changes: Partial<UserMetadata> }
  'user:logout': { userId: string }
  
  // Chat events
  'chat:message-received': { messageId: string, from: string, content: string }
  'chat:typing-start': { from: string, chatId: string }
  
  // Payment events
  'payment:invoice-created': { invoiceId: string, amount: number }
  'payment:received': { invoiceId: string, amount: number }
  
  // Relay events
  'relay:connected': { url: string }
  'relay:disconnected': { url: string, reason?: string }
}

See: ../01-architecture/event-bus

WalletService 🏦

Purpose: Wallet operations and transaction management Location: src/modules/wallet/services/WalletService.ts Token: SERVICE_TOKENS.WALLET_SERVICE

Key Features:

  • Payment Operations - Send Lightning payments to invoices, addresses, and LNURL
  • Payment Link Management - Create and manage LNURL payment links and Lightning addresses
  • Transaction History - Load and manage comprehensive transaction history
  • Real-time Updates - Add new transactions from WebSocket notifications

Reactive State:

interface WalletService {
  transactions: ComputedRef<PaymentTransaction[]>   // Transaction history
  payLinks: ComputedRef<PayLink[]>                 // Created payment links
  isCreatingPayLink: ComputedRef<boolean>          // Pay link creation state
  isSendingPayment: ComputedRef<boolean>           // Payment sending state
  error: ComputedRef<string | null>                // Error state
}

Key Methods:

  • sendPayment(request: SendPaymentRequest) - Send Lightning payment
  • createReceiveAddress(params) - Create LNURL payment link with Lightning address
  • deletePayLink(linkId: string) - Remove payment link
  • addTransaction(payment) - Add transaction from WebSocket notification
  • refresh() - Reload transactions and payment links

WalletWebSocketService

Purpose: Real-time wallet balance updates via WebSocket Location: src/modules/wallet/services/WalletWebSocketService.ts Token: SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE

Key Features:

  • Real-time Connection - WebSocket connection to LNbits for instant updates
  • Balance Synchronization - Smart balance updates with unit conversion handling
  • Payment Notifications - Toast notifications for incoming payments
  • Connection Management - Automatic reconnection with exponential backoff
  • Battery Optimization - VisibilityService integration for mobile efficiency

Reactive State:

interface WalletWebSocketService {
  isConnected: Ref<boolean>                        // WebSocket connection status
  connectionStatus: Ref<string>                    // Connection state details
}

Connection States:

  • disconnected - Not connected
  • connecting - Connection in progress
  • connected - Successfully connected
  • reconnecting - Attempting to reconnect
  • error - Connection failed
  • failed - Max reconnection attempts reached

Key Methods:

  • reconnect() - Manual reconnection trigger
  • disconnect() - Graceful disconnection
  • cleanup() - Service disposal

WebSocket Integration:

  • Connects to wss://lnbits-server/api/v1/ws/{walletInkey}
  • Handles incoming/outgoing payment balance differences
  • Updates PaymentService with converted balance (sats → millisats)
  • Integrates with WalletService for transaction updates

Service Lifecycle

Initialization Phase

Services are initialized in dependency order during application startup:

// 1. Base services (no dependencies)
await authService.initialize()
await storageService.initialize()

// 2. Infrastructure services (depend on base services)
await relayHub.initialize()
await toastService.initialize()

// 3. Feature services (depend on infrastructure)
await chatService.initialize()
await eventsService.initialize()

Service Dependencies

Services declare their dependencies through constructor injection:

class ChatService extends BaseService {
  constructor(
    private auth = injectService(SERVICE_TOKENS.AUTH_SERVICE),
    private relayHub = injectService(SERVICE_TOKENS.RELAY_HUB),
    private storage = injectService(SERVICE_TOKENS.STORAGE_SERVICE),
    private eventBus = injectService(SERVICE_TOKENS.EVENT_BUS)
  ) {
    super()
  }
}

Disposal Phase

Services are disposed in reverse dependency order during application shutdown:

async dispose(): Promise<void> {
  // Clean up subscriptions
  this.subscriptions.forEach(sub => sub.close())
  
  // Save persistent state
  await this.storage.setUserData('chat:messages', this.messages.value)
  
  // Mark as disposed
  this.isDisposed.value = true
}

Dependency Injection

Service Tokens

Type-safe service tokens prevent runtime errors and enable proper TypeScript inference:

export const SERVICE_TOKENS = {
  AUTH_SERVICE: Symbol('AUTH_SERVICE') as InjectionKey<AuthService>,
  RELAY_HUB: Symbol('RELAY_HUB') as InjectionKey<RelayHub>,
  STORAGE_SERVICE: Symbol('STORAGE_SERVICE') as InjectionKey<StorageService>,
  TOAST_SERVICE: Symbol('TOAST_SERVICE') as InjectionKey<ToastService>,
} as const

Service Registration

Services are registered during module installation:

// In base module installation
export async function installBaseModule(app: App) {
  // Create service instances
  const authService = new AuthService()
  const relayHub = new RelayHub()
  const storageService = new StorageService()
  
  // Register in container
  container.provide(SERVICE_TOKENS.AUTH_SERVICE, authService)
  container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
  container.provide(SERVICE_TOKENS.STORAGE_SERVICE, storageService)
  
  // Initialize services
  await authService.initialize()
  await relayHub.initialize()
}

Service Consumption

Services are injected where needed using type-safe injection:

// In composables
export function useAuth() {
  const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
  
  return {
    user: authService.user,
    login: authService.login,
    logout: authService.logout
  }
}

// In components
<script setup>
const toast = injectService(SERVICE_TOKENS.TOAST_SERVICE)
const handleSuccess = () => toast.operation.success('Action completed!')
</script>

Service Development

Creating a New Service

1. Service Class Implementation

// src/core/services/MyService.ts
export class MyService extends BaseService {
  // Reactive state
  private readonly _data = ref<MyData[]>([])
  private readonly _isLoading = ref(false)
  
  // Public readonly access to state
  public readonly data = readonly(this._data)
  public readonly isLoading = readonly(this._isLoading)
  
  constructor(
    private dependency = injectService(SERVICE_TOKENS.DEPENDENCY)
  ) {
    super()
  }
  
  async initialize(): Promise<void> {
    // Initialization logic
    await this.loadInitialData()
    this.isInitialized.value = true
  }
  
  async dispose(): Promise<void> {
    // Cleanup logic
    this._data.value = []
    this.isDisposed.value = true
  }
  
  // Public methods
  async createItem(item: CreateItemRequest): Promise<MyData> {
    this._isLoading.value = true
    try {
      const newItem = await this.dependency.create(item)
      this._data.value.push(newItem)
      return newItem
    } finally {
      this._isLoading.value = false
    }
  }
}

2. Service Token Registration

// Add to SERVICE_TOKENS
export const SERVICE_TOKENS = {
  // ... existing tokens
  MY_SERVICE: Symbol('MY_SERVICE') as InjectionKey<MyService>,
} as const

3. Service Registration in Module

// In module installation
const myService = new MyService()
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
await myService.initialize()

Service Best Practices

Reactive State Management

  • Use ref() for mutable state, readonly() for public access
  • Provide computed properties for derived state
  • Use watch() and watchEffect() for side effects

Error Handling

  • Throw descriptive errors with proper types
  • Use try/catch blocks with proper cleanup
  • Log errors appropriately for debugging

Performance Optimization

  • Implement proper subscription cleanup in dispose()
  • Use debouncing for frequent operations
  • Cache expensive computations with computed()

Testing Services

Service Unit Tests

// tests/unit/services/MyService.test.ts
describe('MyService', () => {
  let service: MyService
  let mockDependency: MockType<DependencyService>
  
  beforeEach(() => {
    // Setup mocks
    mockDependency = createMockService()
    
    // Create service with mocks
    service = new MyService(mockDependency)
  })
  
  afterEach(async () => {
    await service.dispose()
  })
  
  it('should initialize correctly', async () => {
    await service.initialize()
    
    expect(service.isInitialized.value).toBe(true)
    expect(service.data.value).toEqual([])
  })
  
  it('should create items', async () => {
    await service.initialize()
    
    const item = await service.createItem({ name: 'test' })
    
    expect(service.data.value).toContain(item)
    expect(mockDependency.create).toHaveBeenCalledWith({ name: 'test' })
  })
})

Integration Tests

// tests/integration/services/ServiceIntegration.test.ts
describe('Service Integration', () => {
  let container: DIContainer
  
  beforeEach(async () => {
    container = createTestContainer()
    await installTestServices(container)
  })
  
  it('should handle cross-service communication', async () => {
    const authService = container.get(SERVICE_TOKENS.AUTH_SERVICE)
    const chatService = container.get(SERVICE_TOKENS.CHAT_SERVICE)
    
    await authService.login('test-key')
    const message = await chatService.sendMessage('Hello')
    
    expect(message.author).toBe(authService.user.value?.pubkey)
  })
})

See Also

Service Documentation

Architecture References


Tags: #services #architecture #dependency-injection #reactive-state
Last Updated: 2025-09-06
Author: Development Team