import { checkService, type ServiceCheck, type ServiceTarget } from '@bytelyst/monitoring'; import { getNotesMcpToolsForRegistration } from '../mcp/register-note-tools.js'; import { config } from './config.js'; import { initDatastore } from './datastore.js'; import { getEncryptionReadiness } from './field-encrypt.js'; export type DependencyStatus = ServiceCheck['status']; export type DependencyCheck = ServiceCheck & { details?: Record; }; export interface DependencyReadinessReport { overall: 'ready' | 'degraded' | 'down'; timestamp: string; dependencies: DependencyCheck[]; summary: { healthy: number; unhealthy: number; unreachable: number; total: number; }; } type ServiceChecker = (target: ServiceTarget, opts?: { timeoutMs?: number }) => Promise; function elapsedSince(start: number): number { return Math.round(performance.now() - start); } function normalizeServiceBaseUrl(url: string): string { const trimmed = url.replace(/\/+$/, ''); return trimmed.endsWith('/api') ? trimmed.slice(0, -4) : trimmed; } function summarize(checks: DependencyCheck[]): DependencyReadinessReport['summary'] { return { healthy: checks.filter(check => check.status === 'healthy').length, unhealthy: checks.filter(check => check.status === 'unhealthy').length, unreachable: checks.filter(check => check.status === 'unreachable').length, total: checks.length, }; } function overallFrom(summary: DependencyReadinessReport['summary']): DependencyReadinessReport['overall'] { if (summary.healthy === summary.total) { return 'ready'; } if (summary.healthy === 0) { return 'down'; } return 'degraded'; } export async function checkDatastoreReadiness(): Promise { const start = performance.now(); try { const healthy = await initDatastore().isHealthy(); return { name: 'datastore', url: config.DB_PROVIDER, status: healthy ? 'healthy' : 'unhealthy', responseTimeMs: elapsedSince(start), details: { provider: config.DB_PROVIDER }, ...(healthy ? {} : { error: 'Datastore provider reported unhealthy' }), }; } catch (error) { return { name: 'datastore', url: config.DB_PROVIDER, status: 'unreachable', responseTimeMs: elapsedSince(start), error: error instanceof Error ? error.message : String(error), details: { provider: config.DB_PROVIDER }, }; } } export async function checkEncryptionReadiness(): Promise { const start = performance.now(); const readiness = getEncryptionReadiness(); return { name: 'encryption', url: readiness.keyProvider, status: readiness.ready ? 'healthy' : 'unhealthy', responseTimeMs: elapsedSince(start), details: { enabled: readiness.enabled, keyProvider: readiness.keyProvider, }, ...(readiness.ready ? {} : { error: 'Field encryption is not ready' }), }; } async function checkHttpDependency( checker: ServiceChecker, target: ServiceTarget, timeoutMs: number, ): Promise { return checker(target, { timeoutMs }); } async function checkMcpReadiness( checker: ServiceChecker, timeoutMs: number, ): Promise { const baseUrl = normalizeServiceBaseUrl(config.MCP_SERVER_URL); const tools = getNotesMcpToolsForRegistration(); const serviceCheck = await checker({ name: 'mcp-server', url: baseUrl, path: '/health' }, { timeoutMs }); const hasTools = tools.length > 0; const status: DependencyStatus = serviceCheck.status === 'healthy' && hasTools ? 'healthy' : serviceCheck.status; return { ...serviceCheck, name: 'mcp', status: hasTools ? status : 'unhealthy', details: { ...(serviceCheck.details ?? {}), registeredToolCount: tools.length, registeredToolsHealthy: hasTools, }, ...(!hasTools ? { error: 'No NoteLett MCP tools are registered locally' } : {}), }; } export async function getDependencyReadiness(options: { timeoutMs?: number; serviceChecker?: ServiceChecker; } = {}): Promise { const timeoutMs = options.timeoutMs ?? 1500; const serviceChecker = options.serviceChecker ?? checkService; const dependencies = await Promise.all([ checkDatastoreReadiness(), checkEncryptionReadiness(), checkHttpDependency( serviceChecker, { name: 'platform-service', url: normalizeServiceBaseUrl(config.PLATFORM_SERVICE_URL), path: '/health' }, timeoutMs, ), checkHttpDependency( serviceChecker, { name: 'extraction-service', url: normalizeServiceBaseUrl(config.EXTRACTION_SERVICE_URL), path: '/health' }, timeoutMs, ), checkMcpReadiness(serviceChecker, timeoutMs), ]); const summary = summarize(dependencies); return { overall: overallFrom(summary), timestamp: new Date().toISOString(), dependencies, summary, }; }