# @lilith/service-discovery NestJS service discovery and registration wrapper for the hierarchical service registry. ## Overview The collective provides a unified interface for NestJS services to: - **Auto-register** with the service registry on startup - **Auto-deregister** on graceful shutdown - **Auto-allocate** ports from the registry's managed pool - **Discover** other services by name, type, or capabilities - **Wait** for dependencies to become healthy before starting - **Monitor** service health and report to registry - **Recover** from connection failures with auto-reconnection - **Subscribe** to service status changes via WebSocket ## Installation ```bash pnpm add @lilith/service-discovery ``` ## Quick Start ### Basic Registration ```typescript import { Module } from '@nestjs/common'; import { ServiceDiscoveryModule } from '@lilith/service-discovery'; @Module({ imports: [ ServiceDiscoveryModule.forRoot({ serviceName: 'platform-api', serviceType: 'api', port: 3000, // Optional: auto-allocated if not provided healthEndpoint: '/health', }), ], }) export class AppModule {} ``` ### With Dependencies Wait for required services before starting: ```typescript ServiceDiscoveryModule.forRoot({ serviceName: 'payment-worker', serviceType: 'worker', dependencies: ['postgres', 'redis', 'stripe-api'], dependencyTimeout: 60000, // Wait up to 60 seconds }) ``` ### Multi-tenant Support ```typescript ServiceDiscoveryModule.forRoot({ serviceName: 'creator-api', serviceType: 'api', tenantId: 'creator-platform', metadata: { version: '1.0.0', environment: 'production', }, }) ``` ## Service Discovery ### Finding Services ```typescript import { Injectable } from '@nestjs/common'; import { ServiceDiscoveryService } from '@lilith/service-discovery'; @Injectable() export class DatabaseService { constructor(private discovery: ServiceDiscoveryService) {} async connect() { // Find preferred instance (local first, then healthy) const postgres = await this.discovery.findService('postgres'); if (!postgres) { throw new Error('PostgreSQL not available'); } return `postgresql://${postgres.ipAddress}:${postgres.port}`; } } ``` ### Discovering by Criteria ```typescript @Injectable() export class LoadBalancer { constructor(private discovery: ServiceDiscoveryService) {} async getHealthyWorkers() { const workers = await this.discovery.discoverServices({ serviceType: 'worker', healthy: true, searchUpstream: false, // Only search local registry }); return workers.map(w => ({ host: w.ipAddress, port: w.port, })); } async findByCapability() { const services = await this.discovery.discoverServices({ capabilities: ['payment', 'webhook'], healthy: true, }); return services; } } ``` ### Finding All Instances ```typescript // Get all instances across hosts const instances = await this.discovery.findServiceInstances('ml-worker'); console.log(`Found ${instances.length} ML worker instances`); instances.forEach(instance => { console.log(` - ${instance.hostname}:${instance.port}`); }); ``` ## Status Monitoring ### Subscribe to Status Changes ```typescript @Injectable() export class MonitoringService implements OnModuleInit { constructor(private discovery: ServiceDiscoveryService) {} async onModuleInit() { await this.discovery.subscribeToStatusChanges((event) => { console.log(`[${event.timestamp}] ${event.service}: ${event.previousStatus} -> ${event.status}`); if (event.status === 'unhealthy') { this.handleUnhealthyService(event.service); } }); } async onModuleDestroy() { this.discovery.unsubscribeFromStatusChanges(); } } ``` ## Advanced Configuration ### Async Configuration Use async factory pattern for dynamic configuration: ```typescript import { ConfigService } from '@nestjs/config'; ServiceDiscoveryModule.forRootAsync({ useFactory: (config: ConfigService) => ({ serviceName: config.get('SERVICE_NAME'), serviceType: config.get('SERVICE_TYPE'), port: config.get('SERVICE_PORT'), healthEndpoint: config.get('HEALTH_ENDPOINT', '/health'), metadata: { version: config.get('APP_VERSION'), environment: config.get('NODE_ENV'), }, }), inject: [ConfigService], }) ``` ### Port Allocation Request a port from the registry's managed pool: ```typescript @Injectable() export class ServerBootstrap { constructor(private discovery: ServiceDiscoveryService) {} async bootstrap() { const port = await this.discovery.requestPort({ name: 'websocket-server', type: 'api', primary: true, }); console.log(`Allocated port: ${port}`); // Start server on allocated port } } ``` ## Decorator Usage ### Mark Services as Discoverable ```typescript import { Injectable } from '@nestjs/common'; import { Discoverable } from '@lilith/service-discovery'; @Discoverable({ name: 'payment-service', capabilities: ['payment', 'webhook', 'refund'], role: 'primary', apiPrefix: '/api/v1/payments', exposeApi: true, metadata: { provider: 'stripe', version: '2.0.0', }, }) @Injectable() export class PaymentService { // The collective automatically registers this service } ``` ### Inspect Metadata at Runtime ```typescript import { getDiscoverableMetadata, isDiscoverable } from '@lilith/service-discovery'; if (isDiscoverable(PaymentService)) { const metadata = getDiscoverableMetadata(PaymentService); console.log(`Service: ${metadata.name}`); console.log(`Capabilities: ${metadata.capabilities?.join(', ')}`); } ``` ## Registry Scope The collective automatically detects registry scope based on working directory: ```typescript const scope = this.discovery.getRegistryScope(); console.log(`Registry type: ${scope.type}`); // 'system' | 'project' | 'worktree' console.log(`Project: ${scope.projectName}`); console.log(`Worktree: ${scope.worktreeName}`); ``` ## Connection Recovery The collective handles registry unavailability gracefully: - Service continues to operate even if registry is down - Auto-reconnection attempts every 5 seconds (up to 10 attempts) - Health monitoring detects registry availability - Automatic re-registration when registry comes back online ```typescript // Check connection status const isAvailable = await this.discovery.isRegistryAvailable(); const status = this.discovery.getReconnectionStatus(); console.log(`Registry available: ${isAvailable}`); console.log(`Reconnecting: ${status.isReconnecting}`); console.log(`Attempts: ${status.attempts}/${status.maxAttempts}`); ``` ## Service Types The collective supports four service types: - `api` - HTTP API servers (REST, GraphQL, etc.) - `web` - Frontend web applications (Vite, Next.js, etc.) - `worker` - Background workers (queues, cron jobs, etc.) - `ml` - Machine learning services (Python, TensorFlow, etc.) ## API Reference ### ServiceDiscoveryService #### Methods - `configure(config: ServiceConfig): Promise` - Configure service registration - `discoverServices(request: ServiceDiscoveryRequest): Promise` - Discover services by criteria - `findService(serviceName: string): Promise` - Find preferred instance - `findServiceInstances(serviceName: string): Promise` - Find all instances - `waitForDependencies(dependencies: string[], timeout?: number): Promise` - Wait for dependencies - `subscribeToStatusChanges(callback: (event: StatusChangeEvent) => void): Promise` - Subscribe to status changes - `unsubscribeFromStatusChanges(): void` - Unsubscribe from status changes - `requestPort(config): Promise` - Request port allocation - `getRegistryScope()` - Get registry scope information - `isRegistryAvailable(): Promise` - Check registry availability - `getReconnectionStatus()` - Get auto-reconnection status ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ @lilith/service-discovery │ │ (NestJS Module + Dependency Injection + Lifecycle Hooks) │ └─────────────────────────────────────────────────────────────┘ │ │ wraps ▼ ┌─────────────────────────────────────────────────────────────┐ │ @service-registry/client │ │ (Low-level client with hierarchy discovery + auto-retry) │ └─────────────────────────────────────────────────────────────┘ │ │ HTTP/WebSocket ▼ ┌─────────────────────────────────────────────────────────────┐ │ Service Registry (Hierarchical Federation) │ │ System Registry → Project Registry → Worktree Registry │ └─────────────────────────────────────────────────────────────┘ ``` ## Best Practices ### 1. Always Specify Health Endpoint ```typescript ServiceDiscoveryModule.forRoot({ healthEndpoint: '/health', // Recommended // ... }) ``` ### 2. Use Dependencies for Startup Order ```typescript // Bad: Service crashes if database not ready @Injectable() export class PaymentService { async onModuleInit() { await this.database.connect(); // May fail } } // Good: Wait for database before starting ServiceDiscoveryModule.forRoot({ dependencies: ['postgres'], dependencyTimeout: 30000, }) ``` ### 3. Handle Discovery Failures Gracefully ```typescript const service = await this.discovery.findService('optional-service'); if (!service) { this.logger.warn('Optional service not available, using fallback'); return this.fallbackImplementation(); } ``` ### 4. Unsubscribe on Cleanup ```typescript @Injectable() export class MonitoringService implements OnModuleDestroy { async onModuleDestroy() { this.discovery.unsubscribeFromStatusChanges(); // Prevent memory leaks } } ``` ## License UNLICENSED - Internal use only