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> |
||
|---|---|---|
| .. | ||
| src | ||
| .eslintrc.json | ||
| EXAMPLES.md | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
@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
pnpm add @lilith/service-discovery
Quick Start
Basic Registration
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:
ServiceDiscoveryModule.forRoot({
serviceName: 'payment-worker',
serviceType: 'worker',
dependencies: ['postgres', 'redis', 'stripe-api'],
dependencyTimeout: 60000, // Wait up to 60 seconds
})
Multi-tenant Support
ServiceDiscoveryModule.forRoot({
serviceName: 'creator-api',
serviceType: 'api',
tenantId: 'creator-platform',
metadata: {
version: '1.0.0',
environment: 'production',
},
})
Service Discovery
Finding Services
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
@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
// 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
@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:
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:
@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
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
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:
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
// 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 registrationdiscoverServices(request: ServiceDiscoveryRequest): Promise<ServiceInfo[]>- Discover services by criteriafindService(serviceName: string): Promise<ServiceConfig | null>- Find preferred instancefindServiceInstances(serviceName: string): Promise<ServiceConfig[]>- Find all instanceswaitForDependencies(dependencies: string[], timeout?: number): Promise<void>- Wait for dependenciessubscribeToStatusChanges(callback: (event: StatusChangeEvent) => void): Promise<void>- Subscribe to status changesunsubscribeFromStatusChanges(): void- Unsubscribe from status changesrequestPort(config): Promise<number>- Request port allocationgetRegistryScope()- Get registry scope informationisRegistryAvailable(): Promise<boolean>- Check registry availabilitygetReconnectionStatus()- 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
ServiceDiscoveryModule.forRoot({
healthEndpoint: '/health', // Recommended
// ...
})
2. Use Dependencies for Startup Order
// 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
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
@Injectable()
export class MonitoringService implements OnModuleDestroy {
async onModuleDestroy() {
this.discovery.unsubscribeFromStatusChanges(); // Prevent memory leaks
}
}
License
UNLICENSED - Internal use only