Spaces:
Paused
Paused
| import neo4j, { Driver, Session, SessionConfig } from 'neo4j-driver'; | |
| /** | |
| * Neo4jService - Hybrid Cloud/Local Graph Database Connection | |
| * | |
| * Automatically switches between: | |
| * - LOCAL (dev): bolt://localhost:7687 or Docker neo4j:7687 | |
| * - CLOUD (prod): neo4j+s://<id>.databases.neo4j.io (AuraDB) | |
| * | |
| * Features: | |
| * - Self-healing with automatic reconnection | |
| * - Connection pooling | |
| * - Health checks | |
| * - Singleton pattern | |
| */ | |
| export class Neo4jService { | |
| private driver: Driver | null = null; | |
| private isConnecting: boolean = false; | |
| private reconnectAttempts: number = 0; | |
| private maxReconnectAttempts: number = 10; | |
| private reconnectDelay: number = 5000; | |
| constructor() { | |
| this.connect(); | |
| } | |
| /** | |
| * Determines connection URI based on environment | |
| */ | |
| private getConnectionConfig(): { uri: string; user: string; password: string } { | |
| const isProduction = process.env.NODE_ENV === 'production'; | |
| const hasCloudUri = process.env.NEO4J_URI?.includes('neo4j.io'); | |
| // Cloud (AuraDB) - when explicitly configured or in production with cloud URI | |
| if (hasCloudUri) { | |
| console.log('π©οΈ Neo4j Mode: CLOUD (AuraDB)'); | |
| return { | |
| uri: process.env.NEO4J_URI!, | |
| user: process.env.NEO4J_USER || 'neo4j', | |
| password: process.env.NEO4J_PASSWORD || '' | |
| }; | |
| } | |
| // Local Docker (default for dev) | |
| console.log('π³ Neo4j Mode: LOCAL (Docker)'); | |
| return { | |
| uri: process.env.NEO4J_URI || 'bolt://localhost:7687', | |
| user: process.env.NEO4J_USER || 'neo4j', | |
| password: process.env.NEO4J_PASSWORD || 'password' | |
| }; | |
| } | |
| /** | |
| * Initializes connection with self-healing retry logic | |
| */ | |
| private async connect(): Promise<void> { | |
| if (this.driver || this.isConnecting) return; | |
| this.isConnecting = true; | |
| const config = this.getConnectionConfig(); | |
| try { | |
| console.log(`π Connecting to Neural Graph at ${config.uri}...`); | |
| this.driver = neo4j.driver( | |
| config.uri, | |
| neo4j.auth.basic(config.user, config.password), | |
| { | |
| maxConnectionLifetime: 3 * 60 * 60 * 1000, // 3 hours | |
| maxConnectionPoolSize: 50, | |
| connectionAcquisitionTimeout: 10000, // 10 seconds | |
| connectionTimeout: 30000, // 30 seconds | |
| } | |
| ); | |
| // Verify connectivity | |
| await this.driver.verifyConnectivity(); | |
| console.log('π’ NEURAL CORTEX CONNECTED - Neo4j is Online'); | |
| this.reconnectAttempts = 0; | |
| this.isConnecting = false; | |
| } catch (error: any) { | |
| console.error('π΄ Failed to connect to Neural Graph:', error.message); | |
| this.driver = null; | |
| this.isConnecting = false; | |
| // Self-healing: Retry with exponential backoff | |
| if (this.reconnectAttempts < this.maxReconnectAttempts) { | |
| this.reconnectAttempts++; | |
| const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1); | |
| console.log(`β³ Retry attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay/1000}s...`); | |
| setTimeout(() => this.connect(), delay); | |
| } else { | |
| console.error('π Max reconnection attempts reached. Neural Graph is OFFLINE.'); | |
| } | |
| } | |
| } | |
| /** | |
| * Get a session for graph operations | |
| * Triggers reconnect if disconnected | |
| */ | |
| public getSession(config?: SessionConfig): Session { | |
| if (!this.driver) { | |
| this.connect(); | |
| throw new Error('Neural Graph is currently offline. Reconnection in progress...'); | |
| } | |
| return this.driver.session(config); | |
| } | |
| /** | |
| * Execute a Cypher query with automatic session management | |
| */ | |
| public async query<T = any>(cypher: string, params: Record<string, any> = {}): Promise<T[]> { | |
| const session = this.getSession(); | |
| try { | |
| const result = await session.run(cypher, params); | |
| return result.records.map(record => record.toObject() as T); | |
| } finally { | |
| await session.close(); | |
| } | |
| } | |
| /** | |
| * Execute a write transaction | |
| */ | |
| public async write<T = any>(cypher: string, params: Record<string, any> = {}): Promise<T[]> { | |
| const session = this.getSession(); | |
| try { | |
| const result = await session.executeWrite(tx => tx.run(cypher, params)); | |
| return result.records.map(record => record.toObject() as T); | |
| } finally { | |
| await session.close(); | |
| } | |
| } | |
| /** | |
| * Health check for monitoring | |
| */ | |
| public async checkHealth(): Promise<{ status: string; mode: string; latency?: number }> { | |
| if (!this.driver) { | |
| return { status: 'offline', mode: 'unknown' }; | |
| } | |
| const start = Date.now(); | |
| try { | |
| await this.driver.verifyConnectivity(); | |
| const latency = Date.now() - start; | |
| const mode = process.env.NEO4J_URI?.includes('neo4j.io') ? 'cloud' : 'local'; | |
| return { status: 'online', mode, latency }; | |
| } catch (e) { | |
| return { status: 'error', mode: 'unknown' }; | |
| } | |
| } | |
| /** | |
| * Optimized Graph Statistics using System Counts (O(1) complexity) | |
| * Prevents MemoryPoolOutOfMemoryError on large graphs. | |
| * | |
| * OLD HEAVY QUERY: MATCH (n) RETURN count(n)... (DO NOT USE) | |
| * NEW LIGHTWEIGHT QUERY: Uses Neo4j internal store counts which are instant and use 0 memory. | |
| */ | |
| public async getGraphStats(): Promise<{ nodes: number; relationships: number; status: string; mode: string }> { | |
| if (!this.driver) { | |
| return { nodes: 0, relationships: 0, status: 'offline', mode: 'unknown' }; | |
| } | |
| const session = this.driver.session(); | |
| const mode = process.env.NEO4J_URI?.includes('neo4j.io') ? 'cloud' : 'local'; | |
| try { | |
| // Try APOC first (fastest, most efficient) | |
| const result = await session.run(` | |
| CALL apoc.meta.stats() YIELD nodeCount, relCount, labels | |
| RETURN nodeCount, relCount, labels | |
| `).catch(async () => { | |
| // Fallback if APOC is not installed: Use count store (still O(1)) | |
| // These queries use Neo4j's internal count stores, not full scans | |
| console.log('π [Neo4j] APOC not available, using count store fallback'); | |
| return await session.run(` | |
| CALL db.stats.retrieve('GRAPH COUNTS') YIELD data | |
| RETURN data | |
| `).catch(async () => { | |
| // Final fallback: Separate lightweight count queries | |
| console.log('π [Neo4j] Using basic count queries'); | |
| const nodeResult = await session.run('MATCH (n) RETURN count(n) as nodeCount LIMIT 1'); | |
| const relResult = await session.run('MATCH ()-[r]->() RETURN count(r) as relCount LIMIT 1'); | |
| return { | |
| records: [{ | |
| get: (key: string) => { | |
| if (key === 'nodeCount') return { toNumber: () => nodeResult.records[0]?.get('nodeCount')?.toNumber?.() || 0 }; | |
| if (key === 'relCount') return { toNumber: () => relResult.records[0]?.get('relCount')?.toNumber?.() || 0 }; | |
| return null; | |
| }, | |
| has: (key: string) => key === 'nodeCount' || key === 'relCount' | |
| }] | |
| }; | |
| }); | |
| }); | |
| if (result.records.length > 0) { | |
| const record = result.records[0]; | |
| // Handle different return structures depending on query used | |
| let nodes = 0; | |
| let relationships = 0; | |
| if (record.has('nodeCount')) { | |
| const nodeVal = record.get('nodeCount'); | |
| nodes = typeof nodeVal?.toNumber === 'function' ? nodeVal.toNumber() : (nodeVal || 0); | |
| } | |
| if (record.has('relCount')) { | |
| const relVal = record.get('relCount'); | |
| relationships = typeof relVal?.toNumber === 'function' ? relVal.toNumber() : (relVal || 0); | |
| } | |
| if (record.has('data')) { | |
| // Handle db.stats.retrieve format | |
| const data = record.get('data'); | |
| nodes = data?.nodes || 0; | |
| relationships = data?.relationships || 0; | |
| } | |
| return { | |
| nodes, | |
| relationships, | |
| status: 'online', | |
| mode | |
| }; | |
| } | |
| return { nodes: 0, relationships: 0, status: 'online', mode }; | |
| } catch (error) { | |
| console.error('π [Neo4j] getGraphStats error:', error); | |
| // Let Self-Healing handle the error reporting | |
| throw error; | |
| } finally { | |
| await session.close(); | |
| } | |
| } | |
| /** | |
| * Graceful shutdown | |
| */ | |
| public async disconnect(): Promise<void> { | |
| if (this.driver) { | |
| await this.driver.close(); | |
| this.driver = null; | |
| console.log('π Neural Graph connection closed.'); | |
| } | |
| } | |
| /** | |
| * Check if connected | |
| */ | |
| public isConnected(): boolean { | |
| return this.driver !== null; | |
| } | |
| } | |
| // Singleton instance | |
| export const neo4jService = new Neo4jService(); | |