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>
468 lines
12 KiB
Markdown
468 lines
12 KiB
Markdown
# @lilith/service-discovery Examples
|
|
|
|
Practical examples showing how to use the service discovery module in various scenarios.
|
|
|
|
## Example 1: Basic API Service
|
|
|
|
```typescript
|
|
// app.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { ServiceDiscoveryModule } from '@lilith/service-discovery';
|
|
import { PaymentModule } from './payment/payment.module';
|
|
|
|
@Module({
|
|
imports: [
|
|
ServiceDiscoveryModule.forRoot({
|
|
serviceName: 'platform-api',
|
|
serviceType: 'api',
|
|
port: 3000,
|
|
healthEndpoint: '/api/health',
|
|
metadata: {
|
|
version: '1.0.0',
|
|
environment: process.env.NODE_ENV || 'development',
|
|
},
|
|
}),
|
|
PaymentModule,
|
|
],
|
|
})
|
|
export class AppModule {}
|
|
```
|
|
|
|
## Example 2: Worker with Database Dependency
|
|
|
|
```typescript
|
|
// worker.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { ServiceDiscoveryModule } from '@lilith/service-discovery';
|
|
import { QueueProcessor } from './queue.processor';
|
|
|
|
@Module({
|
|
imports: [
|
|
ServiceDiscoveryModule.forRoot({
|
|
serviceName: 'payment-worker',
|
|
serviceType: 'worker',
|
|
// Wait for these services before starting
|
|
dependencies: ['postgres', 'redis'],
|
|
dependencyTimeout: 60000,
|
|
metadata: {
|
|
queue: 'payment-processing',
|
|
},
|
|
}),
|
|
],
|
|
providers: [QueueProcessor],
|
|
})
|
|
export class WorkerModule {}
|
|
```
|
|
|
|
## Example 3: Service Discovery for Load Balancing
|
|
|
|
```typescript
|
|
// load-balancer.service.ts
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { ServiceDiscoveryService } from '@lilith/service-discovery';
|
|
|
|
@Injectable()
|
|
export class LoadBalancerService {
|
|
private readonly logger = new Logger(LoadBalancerService.name);
|
|
|
|
constructor(private discovery: ServiceDiscoveryService) {}
|
|
|
|
async getHealthyWorkers(): Promise<Array<{ host: string; port: number }>> {
|
|
const workers = await this.discovery.discoverServices({
|
|
serviceType: 'worker',
|
|
healthy: true,
|
|
searchUpstream: false, // Only local registry
|
|
});
|
|
|
|
this.logger.log(`Found ${workers.length} healthy workers`);
|
|
|
|
return workers.map(worker => ({
|
|
host: worker.ipAddress || 'localhost',
|
|
port: worker.port || 0,
|
|
}));
|
|
}
|
|
|
|
async distributeTask(task: any): Promise<void> {
|
|
const workers = await this.getHealthyWorkers();
|
|
|
|
if (workers.length === 0) {
|
|
throw new Error('No healthy workers available');
|
|
}
|
|
|
|
// Simple round-robin distribution
|
|
const worker = workers[Math.floor(Math.random() * workers.length)];
|
|
|
|
this.logger.log(`Distributing task to ${worker.host}:${worker.port}`);
|
|
// Send task to worker...
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example 4: Database Connection Service
|
|
|
|
```typescript
|
|
// database.service.ts
|
|
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
|
import { ServiceDiscoveryService } from '@lilith/service-discovery';
|
|
import { Pool } from 'pg';
|
|
|
|
@Injectable()
|
|
export class DatabaseService implements OnModuleInit {
|
|
private readonly logger = new Logger(DatabaseService.name);
|
|
private pool?: Pool;
|
|
|
|
constructor(private discovery: ServiceDiscoveryService) {}
|
|
|
|
async onModuleInit() {
|
|
await this.connectToDatabase();
|
|
}
|
|
|
|
private async connectToDatabase(): Promise<void> {
|
|
// Find PostgreSQL instance
|
|
const postgres = await this.discovery.findService('postgres');
|
|
|
|
if (!postgres) {
|
|
throw new Error('PostgreSQL service not found in registry');
|
|
}
|
|
|
|
const connectionString = `postgresql://user:pass@${postgres.ipAddress}:${postgres.port}/mydb`;
|
|
|
|
this.logger.log(`Connecting to PostgreSQL at ${postgres.ipAddress}:${postgres.port}`);
|
|
|
|
this.pool = new Pool({ connectionString });
|
|
}
|
|
|
|
async query(sql: string, params?: any[]) {
|
|
if (!this.pool) {
|
|
throw new Error('Database not connected');
|
|
}
|
|
return this.pool.query(sql, params);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example 5: Service Status Monitoring
|
|
|
|
```typescript
|
|
// monitoring.service.ts
|
|
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
|
|
import { ServiceDiscoveryService } from '@lilith/service-discovery';
|
|
|
|
@Injectable()
|
|
export class MonitoringService implements OnModuleInit, OnModuleDestroy {
|
|
private readonly logger = new Logger(MonitoringService.name);
|
|
private metrics = new Map<string, { healthy: number; unhealthy: number }>();
|
|
|
|
constructor(private discovery: ServiceDiscoveryService) {}
|
|
|
|
async onModuleInit() {
|
|
await this.discovery.subscribeToStatusChanges((event) => {
|
|
this.logger.log(
|
|
`[${event.timestamp}] ${event.service}: ${event.previousStatus} -> ${event.status}`
|
|
);
|
|
|
|
this.updateMetrics(event.service, event.status);
|
|
|
|
if (event.status === 'unhealthy') {
|
|
this.handleUnhealthyService(event.service, event.reason);
|
|
}
|
|
});
|
|
|
|
this.logger.log('Started monitoring service status changes');
|
|
}
|
|
|
|
async onModuleDestroy() {
|
|
this.discovery.unsubscribeFromStatusChanges();
|
|
this.logger.log('Stopped monitoring service status changes');
|
|
}
|
|
|
|
private updateMetrics(service: string, status: string): void {
|
|
if (!this.metrics.has(service)) {
|
|
this.metrics.set(service, { healthy: 0, unhealthy: 0 });
|
|
}
|
|
|
|
const metrics = this.metrics.get(service)!;
|
|
|
|
if (status === 'healthy') {
|
|
metrics.healthy++;
|
|
} else if (status === 'unhealthy') {
|
|
metrics.unhealthy++;
|
|
}
|
|
}
|
|
|
|
private handleUnhealthyService(service: string, reason?: string): void {
|
|
this.logger.error(`Service ${service} is unhealthy: ${reason || 'unknown reason'}`);
|
|
|
|
// The collective could trigger alerts, circuit breakers, etc.
|
|
// Example: Send alert to monitoring system
|
|
}
|
|
|
|
getMetrics() {
|
|
return Object.fromEntries(this.metrics);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example 6: Multi-Tenant Service
|
|
|
|
```typescript
|
|
// tenant-api.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { ServiceDiscoveryModule } from '@lilith/service-discovery';
|
|
|
|
@Module({
|
|
imports: [
|
|
ServiceDiscoveryModule.forRoot({
|
|
serviceName: 'creator-api',
|
|
serviceType: 'api',
|
|
tenantId: 'creator-platform',
|
|
port: 4000,
|
|
healthEndpoint: '/health',
|
|
metadata: {
|
|
tenant: 'creator-platform',
|
|
version: '2.0.0',
|
|
capabilities: 'content-management,analytics',
|
|
},
|
|
}),
|
|
],
|
|
})
|
|
export class TenantApiModule {}
|
|
```
|
|
|
|
## Example 7: Async Configuration with Environment Variables
|
|
|
|
```typescript
|
|
// app.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
import { ServiceDiscoveryModule } from '@lilith/service-discovery';
|
|
|
|
@Module({
|
|
imports: [
|
|
ConfigModule.forRoot({
|
|
isGlobal: true,
|
|
}),
|
|
ServiceDiscoveryModule.forRootAsync({
|
|
useFactory: (config: ConfigService) => ({
|
|
serviceName: config.get('SERVICE_NAME', 'default-service'),
|
|
serviceType: config.get('SERVICE_TYPE', 'api') as 'api' | 'web' | 'worker' | 'ml',
|
|
port: config.get<number>('SERVICE_PORT'),
|
|
healthEndpoint: config.get('HEALTH_ENDPOINT', '/health'),
|
|
primary: config.get('PRIMARY_INSTANCE') === 'true',
|
|
dependencies: config.get('DEPENDENCIES')?.split(',') || [],
|
|
metadata: {
|
|
version: config.get('APP_VERSION', '1.0.0'),
|
|
environment: config.get('NODE_ENV', 'development'),
|
|
region: config.get('REGION', 'us-east-1'),
|
|
},
|
|
}),
|
|
inject: [ConfigService],
|
|
}),
|
|
],
|
|
})
|
|
export class AppModule {}
|
|
```
|
|
|
|
## Example 8: Port Allocation for Dynamic Services
|
|
|
|
```typescript
|
|
// websocket-server.service.ts
|
|
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
|
import { ServiceDiscoveryService } from '@lilith/service-discovery';
|
|
import { Server } from 'socket.io';
|
|
|
|
@Injectable()
|
|
export class WebSocketServerService implements OnModuleInit {
|
|
private readonly logger = new Logger(WebSocketServerService.name);
|
|
private io?: Server;
|
|
|
|
constructor(private discovery: ServiceDiscoveryService) {}
|
|
|
|
async onModuleInit() {
|
|
// Request a port from the registry
|
|
const port = await this.discovery.requestPort({
|
|
name: 'websocket-server',
|
|
type: 'api',
|
|
primary: true,
|
|
});
|
|
|
|
this.logger.log(`Starting WebSocket server on port ${port}`);
|
|
|
|
this.io = new Server(port, {
|
|
cors: { origin: '*' },
|
|
});
|
|
|
|
this.io.on('connection', (socket) => {
|
|
this.logger.log(`Client connected: ${socket.id}`);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example 9: Discovering Services by Capability
|
|
|
|
```typescript
|
|
// service-mesh.service.ts
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { ServiceDiscoveryService } from '@lilith/service-discovery';
|
|
|
|
@Injectable()
|
|
export class ServiceMeshService {
|
|
private readonly logger = new Logger(ServiceMeshService.name);
|
|
|
|
constructor(private discovery: ServiceDiscoveryService) {}
|
|
|
|
async findPaymentProviders() {
|
|
const services = await this.discovery.discoverServices({
|
|
capabilities: ['payment', 'webhook'],
|
|
healthy: true,
|
|
});
|
|
|
|
this.logger.log(`Found ${services.length} payment providers`);
|
|
|
|
return services.map(s => ({
|
|
name: s.name,
|
|
capabilities: s.metadata?.capabilities,
|
|
endpoint: `http://${s.ipAddress}:${s.port}`,
|
|
}));
|
|
}
|
|
|
|
async findAllMlServices() {
|
|
const services = await this.discovery.discoverServices({
|
|
serviceType: 'ml',
|
|
searchUpstream: true, // Search parent registries too
|
|
});
|
|
|
|
return services;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example 10: Discoverable Service with Decorator
|
|
|
|
```typescript
|
|
// payment.service.ts
|
|
import { Injectable } from '@nestjs/common';
|
|
import { Discoverable } from '@lilith/service-discovery';
|
|
|
|
@Discoverable({
|
|
name: 'payment-service',
|
|
capabilities: ['payment', 'webhook', 'refund', 'subscription'],
|
|
role: 'primary',
|
|
apiPrefix: '/api/v1/payments',
|
|
exposeApi: true,
|
|
metadata: {
|
|
provider: 'stripe',
|
|
version: '2.0.0',
|
|
supportedCurrencies: 'USD,EUR,GBP',
|
|
},
|
|
})
|
|
@Injectable()
|
|
export class PaymentService {
|
|
async processPayment(amount: number, currency: string) {
|
|
// Payment processing logic
|
|
}
|
|
|
|
async handleWebhook(payload: any) {
|
|
// Webhook handling logic
|
|
}
|
|
|
|
async refund(transactionId: string) {
|
|
// Refund logic
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example 11: Checking Registry Availability
|
|
|
|
```typescript
|
|
// health.controller.ts
|
|
import { Controller, Get } from '@nestjs/common';
|
|
import { ServiceDiscoveryService } from '@lilith/service-discovery';
|
|
|
|
@Controller('health')
|
|
export class HealthController {
|
|
constructor(private discovery: ServiceDiscoveryService) {}
|
|
|
|
@Get()
|
|
async getHealth() {
|
|
const registryAvailable = await this.discovery.isRegistryAvailable();
|
|
const reconnectionStatus = this.discovery.getReconnectionStatus();
|
|
const scope = this.discovery.getRegistryScope();
|
|
|
|
return {
|
|
status: 'ok',
|
|
registry: {
|
|
available: registryAvailable,
|
|
reconnecting: reconnectionStatus.isReconnecting,
|
|
attempts: reconnectionStatus.attempts,
|
|
maxAttempts: reconnectionStatus.maxAttempts,
|
|
scope: {
|
|
type: scope?.type,
|
|
project: scope?.projectName,
|
|
worktree: scope?.worktreeName,
|
|
},
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example 12: Graceful Degradation
|
|
|
|
```typescript
|
|
// cache.service.ts
|
|
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
|
import { ServiceDiscoveryService } from '@lilith/service-discovery';
|
|
|
|
@Injectable()
|
|
export class CacheService implements OnModuleInit {
|
|
private readonly logger = new Logger(CacheService.name);
|
|
private redisAvailable = false;
|
|
private localCache = new Map<string, any>();
|
|
|
|
constructor(private discovery: ServiceDiscoveryService) {}
|
|
|
|
async onModuleInit() {
|
|
await this.checkRedisAvailability();
|
|
}
|
|
|
|
private async checkRedisAvailability() {
|
|
const redis = await this.discovery.findService('redis');
|
|
|
|
if (redis) {
|
|
this.redisAvailable = true;
|
|
this.logger.log('Redis available, using distributed cache');
|
|
} else {
|
|
this.logger.warn('Redis not available, falling back to local cache');
|
|
}
|
|
}
|
|
|
|
async get(key: string): Promise<any> {
|
|
if (this.redisAvailable) {
|
|
// Use Redis
|
|
return this.getFromRedis(key);
|
|
} else {
|
|
// Fallback to local cache
|
|
return this.localCache.get(key);
|
|
}
|
|
}
|
|
|
|
async set(key: string, value: any, ttl?: number): Promise<void> {
|
|
if (this.redisAvailable) {
|
|
await this.setInRedis(key, value, ttl);
|
|
} else {
|
|
this.localCache.set(key, value);
|
|
}
|
|
}
|
|
|
|
private async getFromRedis(key: string): Promise<any> {
|
|
// Redis implementation
|
|
}
|
|
|
|
private async setInRedis(key: string, value: any, ttl?: number): Promise<void> {
|
|
// Redis implementation
|
|
}
|
|
}
|
|
```
|