network/src/db/core/connection.ts
2025-05-16 11:56:23 +03:00

251 lines
7.2 KiB
TypeScript

import { createServiceLogger } from '../../utils/logger';
import { init as initIpfs, stop as stopIpfs } from '../../ipfs/ipfsService';
import { init as initOrbitDB } from '../../orbit/orbitDBService';
import { DBConnection, ErrorCode } from '../types';
import { DBError } from './error';
const logger = createServiceLogger('DB_CONNECTION');
// Connection pool of database instances
const connections = new Map<string, DBConnection>();
let defaultConnectionId: string | null = null;
let cleanupInterval: NodeJS.Timeout | null = null;
// Configuration
const CONNECTION_TIMEOUT = 3600000; // 1 hour in milliseconds
const CLEANUP_INTERVAL = 300000; // 5 minutes in milliseconds
const MAX_RETRY_ATTEMPTS = 3;
const RETRY_DELAY = 2000; // 2 seconds
/**
* Initialize the database service
* This abstracts away OrbitDB and IPFS from the end user
*/
export const init = async (connectionId?: string): Promise<string> => {
// Start connection cleanup interval if not already running
if (!cleanupInterval) {
cleanupInterval = setInterval(cleanupStaleConnections, CLEANUP_INTERVAL);
logger.info(`Connection cleanup scheduled every ${CLEANUP_INTERVAL / 60000} minutes`);
}
const connId = connectionId || `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.info(`Initializing DB service with connection ID: ${connId}`);
let attempts = 0;
let lastError: any = null;
// Retry initialization with exponential backoff
while (attempts < MAX_RETRY_ATTEMPTS) {
try {
// Initialize IPFS with retry logic
const ipfsInstance = await initIpfs().catch((error) => {
logger.error(
`IPFS initialization failed (attempt ${attempts + 1}/${MAX_RETRY_ATTEMPTS}):`,
error,
);
throw error;
});
// Initialize OrbitDB
const orbitdbInstance = await initOrbitDB().catch((error) => {
logger.error(
`OrbitDB initialization failed (attempt ${attempts + 1}/${MAX_RETRY_ATTEMPTS}):`,
error,
);
throw error;
});
// Store connection in pool
connections.set(connId, {
ipfs: ipfsInstance,
orbitdb: orbitdbInstance,
timestamp: Date.now(),
isActive: true,
});
// Set as default if no default exists
if (!defaultConnectionId) {
defaultConnectionId = connId;
}
logger.info(`DB service initialized successfully with connection ID: ${connId}`);
return connId;
} catch (error) {
lastError = error;
attempts++;
if (attempts >= MAX_RETRY_ATTEMPTS) {
logger.error(
`Failed to initialize DB service after ${MAX_RETRY_ATTEMPTS} attempts:`,
error,
);
break;
}
// Wait before retrying with exponential backoff
const delay = RETRY_DELAY * Math.pow(2, attempts - 1);
logger.info(
`Retrying initialization in ${delay}ms (attempt ${attempts + 1}/${MAX_RETRY_ATTEMPTS})...`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new DBError(
ErrorCode.INITIALIZATION_FAILED,
`Failed to initialize database service after ${MAX_RETRY_ATTEMPTS} attempts`,
lastError,
);
};
/**
* Get the active connection
*/
export const getConnection = (connectionId?: string): DBConnection => {
const connId = connectionId || defaultConnectionId;
if (!connId || !connections.has(connId)) {
throw new DBError(
ErrorCode.NOT_INITIALIZED,
`No active database connection found${connectionId ? ` for ID: ${connectionId}` : ''}`,
);
}
const connection = connections.get(connId)!;
if (!connection.isActive) {
throw new DBError(ErrorCode.CONNECTION_ERROR, `Connection ${connId} is no longer active`);
}
// Update the timestamp to mark connection as recently used
connection.timestamp = Date.now();
return connection;
};
/**
* Cleanup stale connections to prevent memory leaks
*/
export const cleanupStaleConnections = (): void => {
try {
const now = Date.now();
let removedCount = 0;
// Identify stale connections (older than CONNECTION_TIMEOUT)
for (const [id, connection] of connections.entries()) {
if (connection.isActive && now - connection.timestamp > CONNECTION_TIMEOUT) {
logger.info(
`Closing stale connection: ${id} (inactive for ${(now - connection.timestamp) / 60000} minutes)`,
);
// Close connection asynchronously (don't await to avoid blocking)
closeConnection(id)
.then((success) => {
if (success) {
logger.info(`Successfully closed stale connection: ${id}`);
} else {
logger.warn(`Failed to close stale connection: ${id}`);
}
})
.catch((error) => {
logger.error(`Error closing stale connection ${id}:`, error);
});
removedCount++;
} else if (!connection.isActive) {
// Remove inactive connections from the map
connections.delete(id);
removedCount++;
}
}
if (removedCount > 0) {
logger.info(`Cleaned up ${removedCount} stale or inactive connections`);
}
} catch (error) {
logger.error('Error during connection cleanup:', error);
}
};
/**
* Close a specific database connection
*/
export const closeConnection = async (connectionId: string): Promise<boolean> => {
if (!connections.has(connectionId)) {
return false;
}
try {
const connection = connections.get(connectionId)!;
// Stop OrbitDB
if (connection.orbitdb) {
await connection.orbitdb.stop();
}
// Mark connection as inactive
connection.isActive = false;
// If this was the default connection, clear it
if (defaultConnectionId === connectionId) {
defaultConnectionId = null;
// Try to find another active connection to be the default
for (const [id, conn] of connections.entries()) {
if (conn.isActive) {
defaultConnectionId = id;
break;
}
}
}
// Remove the connection from the pool
connections.delete(connectionId);
logger.info(`Closed database connection: ${connectionId}`);
return true;
} catch (error) {
logger.error(`Error closing connection ${connectionId}:`, error);
return false;
}
};
/**
* Stop all database connections
*/
export const stop = async (): Promise<void> => {
try {
// Stop the cleanup interval
if (cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
// Close all connections
const promises: Promise<boolean>[] = [];
for (const [id, connection] of connections.entries()) {
if (connection.isActive) {
promises.push(closeConnection(id));
}
}
// Wait for all connections to close
await Promise.allSettled(promises);
// Stop IPFS if needed
const ipfs = connections.get(defaultConnectionId || '')?.ipfs;
if (ipfs) {
await stopIpfs();
}
// Clear all connections
connections.clear();
defaultConnectionId = null;
logger.info('All DB connections stopped successfully');
} catch (error: any) {
logger.error('Error stopping DB connections:', error);
throw new Error(`Failed to stop database connections: ${error.message}`);
}
};