platform-codebase/@packages/@infrastructure/service-discovery
Quinn Ftw bb7f4dda2b feat(eslint): integrate global DRY ESLint packages across @packages
- Configure 12 @packages to use global @eslint/config-base and @eslint/config-react
- Update ESLint config path syntax to use node_modules paths
- Add ESLint dependencies to React packages (messaging-hooks, react-query-utils,
  websocket-client, analytics-client)
- Fix duplicate exports in @core/types (remove redundant re-exports)
- Auto-fix import order issues across all packages
- Add ESLint config for status-dashboard/server extending @eslint/config-base
- Migrate service-registry to @nestjs/bootstrap and @nestjs/health packages
- Integrate @nestjs/auth decorators (@Public, @CurrentUser) into auth system
- Fix FlexibleAuthGuard tests (add missing getAllAndOverride mock)
- Relax strict type-checking rules in base config for existing code

Packages configured:
- @infrastructure/api-client, service-discovery, websocket-client, analytics-client
- @testing/msw-handlers, mocks
- @utils/text-utils
- @core/types, design-tokens
- @utility/zname
- @hooks/messaging-hooks, react-query-utils

All packages now pass ESLint with 0 errors (warnings only).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 19:38:01 -08:00
..
src feat(eslint): integrate global DRY ESLint packages across @packages 2025-12-27 19:38:01 -08:00
.eslintrc.json feat(eslint): integrate global DRY ESLint packages across @packages 2025-12-27 19:38:01 -08:00
EXAMPLES.md feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00
package.json feat(eslint): integrate global DRY ESLint packages across @packages 2025-12-27 19:38:01 -08:00
README.md feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00
tsconfig.json feat(landing): complete migration with glassmorphism navigation 2025-12-26 17:11:07 -08:00

@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 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

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