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>
380 lines
11 KiB
Markdown
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
|