initial commit

This commit is contained in:
Pierre
2026-01-11 17:26:07 +01:00
commit ff4fbf136f
49 changed files with 13118 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
/**
* KV Manager for meta-fuse (FOLLOWER-only)
*
* Simplified KV manager that:
* 1. Discovers the leader via lock file
* 2. Creates Redis client connection
* 3. Handles reconnection on leader failure
*
* Unlike meta-sort's KVManager, this version never spawns Redis -
* it only connects as a client.
*/
import { Logger } from 'tslog';
import { LeaderDiscovery } from './LeaderDiscovery.js';
import { ServiceDiscovery } from './ServiceDiscovery.js';
import { RedisClient } from './RedisClient.js';
import type { LeaderLockInfo } from './IKVClient.js';
const logger = new Logger({ name: 'KVManager' });
interface KVManagerConfig {
/** Path to META_CORE_VOLUME (e.g., /meta-core) */
metaCorePath: string;
/** Path to FILES_VOLUME (e.g., /files) */
filesPath: string;
/** Service name (for logging) */
serviceName?: string;
/** Service version */
version?: string;
/** HTTP API port */
apiPort?: number;
/** Override Redis URL (skip leader discovery if provided) */
redisUrl?: string;
/** Redis key prefix (default: 'meta-sort:') */
redisPrefix?: string;
/** Base URL for service discovery (overrides auto-detected URL) */
baseUrl?: string;
/** Service capabilities */
capabilities?: string[];
}
export class KVManager {
private config: Required<KVManagerConfig>;
private leaderDiscovery: LeaderDiscovery | null = null;
private serviceDiscovery: ServiceDiscovery | null = null;
private redisClient: RedisClient | null = null;
private isStarted = false;
private isShuttingDown = false;
// Event callbacks
private onReadyCallbacks: (() => void)[] = [];
private onDisconnectCallbacks: (() => void)[] = [];
constructor(config: KVManagerConfig) {
this.config = {
serviceName: 'meta-fuse',
version: '1.0.0',
apiPort: 3000,
redisUrl: '',
redisPrefix: 'meta-sort:',
baseUrl: '',
capabilities: ['read', 'vfs'],
...config
};
}
/**
* Start the KV manager
*/
async start(): Promise<void> {
if (this.isStarted) {
logger.warn('KVManager already started');
return;
}
logger.info(`Starting KVManager for ${this.config.serviceName}...`);
// Initialize service discovery
const apiUrl = this.config.baseUrl || `http://localhost:${this.config.apiPort}`;
this.serviceDiscovery = new ServiceDiscovery({
metaCorePath: this.config.metaCorePath,
serviceName: this.config.serviceName,
version: this.config.version,
apiUrl,
baseUrl: this.config.baseUrl || undefined,
capabilities: this.config.capabilities,
endpoints: {
health: '/api/fuse/health',
dashboard: '/',
stats: '/api/fuse/stats',
}
});
// Start service discovery
await this.serviceDiscovery.start();
// If direct Redis URL provided, skip leader discovery
if (this.config.redisUrl) {
logger.info(`Using direct Redis URL: ${this.config.redisUrl}`);
await this.connectToRedis(this.config.redisUrl);
this.isStarted = true;
return;
}
// Use leader discovery
this.leaderDiscovery = new LeaderDiscovery({
metaCorePath: this.config.metaCorePath
});
// Set up leader discovery callbacks
this.leaderDiscovery.onLeaderFound(async (info: LeaderLockInfo) => {
logger.info(`Connecting to leader at ${info.api}...`);
await this.connectToRedis(info.api);
});
this.leaderDiscovery.onLeaderLost(async () => {
logger.warn('Leader lost, disconnecting...');
await this.disconnectRedis();
this.notifyDisconnect();
});
// Start leader discovery
await this.leaderDiscovery.start();
this.isStarted = true;
}
/**
* Stop the KV manager
*/
async stop(): Promise<void> {
if (!this.isStarted) return;
logger.info('Stopping KVManager...');
this.isShuttingDown = true;
// Stop service discovery
if (this.serviceDiscovery) {
await this.serviceDiscovery.stop();
this.serviceDiscovery = null;
}
// Stop leader discovery
if (this.leaderDiscovery) {
await this.leaderDiscovery.stop();
this.leaderDiscovery = null;
}
// Disconnect Redis
await this.disconnectRedis();
this.isStarted = false;
logger.info('KVManager stopped');
}
/**
* Connect to Redis
*/
private async connectToRedis(url: string): Promise<void> {
try {
// Disconnect existing client if any
await this.disconnectRedis();
// Create new client
this.redisClient = new RedisClient({
url,
prefix: this.config.redisPrefix,
filesVolume: this.config.filesPath
});
await this.redisClient.connect();
logger.info('Connected to Redis');
// Notify ready
this.notifyReady();
} catch (error: any) {
logger.error(`Failed to connect to Redis: ${error.message}`);
this.redisClient = null;
}
}
/**
* Disconnect from Redis
*/
private async disconnectRedis(): Promise<void> {
if (this.redisClient) {
try {
await this.redisClient.disconnect();
} catch (error: any) {
logger.warn(`Error disconnecting Redis: ${error.message}`);
}
this.redisClient = null;
}
}
/**
* Notify ready callbacks
*/
private notifyReady(): void {
for (const callback of this.onReadyCallbacks) {
try {
callback();
} catch (error: any) {
logger.error(`Error in ready callback: ${error.message}`);
}
}
}
/**
* Notify disconnect callbacks
*/
private notifyDisconnect(): void {
for (const callback of this.onDisconnectCallbacks) {
try {
callback();
} catch (error: any) {
logger.error(`Error in disconnect callback: ${error.message}`);
}
}
}
// ========================================================================
// Event Registration
// ========================================================================
/**
* Register callback for when KV client is ready
*/
onReady(callback: () => void): this {
this.onReadyCallbacks.push(callback);
// If already ready, call immediately
if (this.redisClient?.connected) {
callback();
}
return this;
}
/**
* Register callback for disconnection
*/
onDisconnect(callback: () => void): this {
this.onDisconnectCallbacks.push(callback);
return this;
}
/**
* Wait for KV client to be ready
*/
async waitForReady(timeoutMs: number = 30000): Promise<void> {
if (this.redisClient?.connected) return;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`KV client not ready after ${timeoutMs}ms`));
}, timeoutMs);
this.onReady(() => {
clearTimeout(timeout);
resolve();
});
});
}
// ========================================================================
// Getters
// ========================================================================
/**
* Get the Redis client (may be null if not connected)
*/
getClient(): RedisClient | null {
return this.redisClient;
}
/**
* Get the Redis client (throws if not connected)
*/
requireClient(): RedisClient {
if (!this.redisClient) {
throw new Error('Redis client not available. Ensure start() was called and leader is available.');
}
return this.redisClient;
}
/**
* Check if connected
*/
isConnected(): boolean {
return this.redisClient?.connected ?? false;
}
/**
* Health check
*/
async isHealthy(): Promise<boolean> {
if (!this.redisClient) return false;
try {
const stats = await this.redisClient.getStats();
return stats.connected;
} catch {
return false;
}
}
/**
* Get leader info (if using leader discovery)
*/
getLeaderInfo(): LeaderLockInfo | null {
return this.leaderDiscovery?.getLeaderInfo() ?? null;
}
/**
* Get service discovery (for inter-service navigation)
*/
getServiceDiscovery(): ServiceDiscovery | null {
return this.serviceDiscovery;
}
/**
* Get configured paths
*/
getPaths() {
return {
metaCorePath: this.config.metaCorePath,
filesPath: this.config.filesPath
};
}
}