platform-codebase/@packages/@infrastructure/service-discovery/README.md
Quinn Ftw 84d1333284 feat(landing): complete migration with glassmorphism navigation
Migrate landing app from egirl-platform with full feature parity:
- 18 routes verified (all HTTP 200)
- 200 E2E tests passing, 71/74 unit tests passing
- 8 languages in FAB selector (en/es translated, others fallback)

Add ThemeProvider to App.tsx for styled-components theme context.
Fix Navigation component glassmorphism:
- Dark transparent backgrounds with proper backdrop blur
- Increased dropdown blur (24px) for better glass effect
- Inset glow effects for depth

Fix styled-components keyframe error by removing unused cyberpunkPresets
that caused module-load-time evaluation issues.

Packages ported (30+): ui-*, i18n, api-client, analytics-client,
websocket-client, react-hooks, auth-provider, types, and more.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 17:11:07 -08:00

380 lines
11 KiB
Markdown

# @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<number>('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<void>` - Configure service registration
- `discoverServices(request: ServiceDiscoveryRequest): Promise<ServiceInfo[]>` - Discover services by criteria
- `findService(serviceName: string): Promise<ServiceConfig | null>` - Find preferred instance
- `findServiceInstances(serviceName: string): Promise<ServiceConfig[]>` - Find all instances
- `waitForDependencies(dependencies: string[], timeout?: number): Promise<void>` - Wait for dependencies
- `subscribeToStatusChanges(callback: (event: StatusChangeEvent) => void): Promise<void>` - Subscribe to status changes
- `unsubscribeFromStatusChanges(): void` - Unsubscribe from status changes
- `requestPort(config): Promise<number>` - Request port allocation
- `getRegistryScope()` - Get registry scope information
- `isRegistryAvailable(): Promise<boolean>` - 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