import * as plugins from './plugins.js'; import * as path from 'path'; import * as url from 'url'; import { EventEmitter } from 'events'; /** * A single oplog entry returned from the Rust engine. */ export interface IOpLogEntry { seq: number; timestampMs: number; op: 'insert' | 'update' | 'delete'; db: string; collection: string; documentId: string; document: Record | null; previousDocument: Record | null; } /** * Aggregate oplog statistics. */ export interface IOpLogStats { currentSeq: number; totalEntries: number; oldestSeq: number; entriesByOp: { insert: number; update: number; delete: number; }; } /** * Result of a getOpLog query. */ export interface IOpLogResult { entries: IOpLogEntry[]; currentSeq: number; totalEntries: number; } /** * Result of a revertToSeq command. */ export interface IRevertResult { dryRun: boolean; reverted: number; targetSeq?: number; entries?: IOpLogEntry[]; errors?: string[]; } /** * A collection info entry. */ export interface ICollectionInfo { db: string; name: string; count: number; } /** * Result of a getDocuments query. */ export interface IDocumentsResult { documents: Record[]; total: number; } /** * Server metrics. */ export interface ISmartDbMetrics { databases: number; collections: number; oplogEntries: number; oplogCurrentSeq: number; uptimeSeconds: number; } /** * Type-safe command definitions for the RustDb IPC protocol. */ type TSmartDbCommands = { start: { params: { config: ISmartDbRustConfig }; result: { connectionUri: string } }; stop: { params: Record; result: void }; getStatus: { params: Record; result: { running: boolean } }; getMetrics: { params: Record; result: ISmartDbMetrics }; getOpLog: { params: { sinceSeq?: number; limit?: number; db?: string; collection?: string }; result: IOpLogResult; }; getOpLogStats: { params: Record; result: IOpLogStats }; revertToSeq: { params: { seq: number; dryRun?: boolean }; result: IRevertResult; }; getCollections: { params: { db?: string }; result: { collections: ICollectionInfo[] }; }; getDocuments: { params: { db: string; collection: string; limit?: number; skip?: number }; result: IDocumentsResult; }; }; /** * Configuration sent to the Rust binary on start. */ interface ISmartDbRustConfig { port?: number; host?: string; socketPath?: string; storage: 'memory' | 'file'; storagePath?: string; persistPath?: string; persistIntervalMs?: number; } /** * Get the package root directory using import.meta.url. * This file is at ts/ts_smartdb/, so package root is 2 levels up. */ function getPackageRoot(): string { const thisDir = path.dirname(url.fileURLToPath(import.meta.url)); return path.resolve(thisDir, '..', '..'); } /** * Map Node.js process.platform/process.arch to tsrust's friendly name suffix. * tsrust names cross-compiled binaries as: rustdb_linux_amd64, rustdb_linux_arm64, etc. */ function getTsrustPlatformSuffix(): string | null { const archMap: Record = { x64: 'amd64', arm64: 'arm64' }; const osMap: Record = { linux: 'linux', darwin: 'macos' }; const os = osMap[process.platform]; const arch = archMap[process.arch]; if (os && arch) { return `${os}_${arch}`; } return null; } /** * Build local search paths for the Rust binary, including dist_rust/ candidates * (built by tsrust) and local development build paths. */ function buildLocalPaths(): string[] { const packageRoot = getPackageRoot(); const suffix = getTsrustPlatformSuffix(); const paths: string[] = []; // dist_rust/ candidates (tsrust cross-compiled output) if (suffix) { paths.push(path.join(packageRoot, 'dist_rust', `rustdb_${suffix}`)); } paths.push(path.join(packageRoot, 'dist_rust', 'rustdb')); // Local dev build paths paths.push(path.resolve(process.cwd(), 'rust', 'target', 'release', 'rustdb')); paths.push(path.resolve(process.cwd(), 'rust', 'target', 'debug', 'rustdb')); return paths; } /** * Bridge between TypeScript SmartdbServer and the Rust binary. * Wraps @push.rocks/smartrust's RustBridge with type-safe command definitions. */ export class RustDbBridge extends EventEmitter { private bridge: plugins.smartrust.RustBridge; constructor() { super(); this.bridge = new plugins.smartrust.RustBridge({ binaryName: 'rustdb', envVarName: 'SMARTDB_RUST_BINARY', platformPackagePrefix: '@push.rocks/smartdb', localPaths: buildLocalPaths(), maxPayloadSize: 10 * 1024 * 1024, // 10 MB }); // Forward events from the inner bridge this.bridge.on('exit', (code: number | null, signal: string | null) => { this.emit('exit', code, signal); }); } /** * Spawn the Rust binary in management mode. * Returns true if the binary was found and spawned successfully. */ public async spawn(): Promise { return this.bridge.spawn(); } /** * Kill the Rust process and clean up. */ public kill(): void { this.bridge.kill(); } /** * Whether the bridge is currently running. */ public get running(): boolean { return this.bridge.running; } // --- Convenience methods for each management command --- public async startDb(config: ISmartDbRustConfig): Promise<{ connectionUri: string }> { return await this.bridge.sendCommand('start', { config }) as { connectionUri: string }; } public async stopDb(): Promise { await this.bridge.sendCommand('stop', {} as Record); } public async getStatus(): Promise<{ running: boolean }> { return await this.bridge.sendCommand('getStatus', {} as Record) as { running: boolean }; } public async getMetrics(): Promise { return this.bridge.sendCommand('getMetrics', {} as Record) as Promise; } public async getOpLog(params: { sinceSeq?: number; limit?: number; db?: string; collection?: string; } = {}): Promise { return this.bridge.sendCommand('getOpLog', params) as Promise; } public async getOpLogStats(): Promise { return this.bridge.sendCommand('getOpLogStats', {} as Record) as Promise; } public async revertToSeq(seq: number, dryRun = false): Promise { return this.bridge.sendCommand('revertToSeq', { seq, dryRun }) as Promise; } public async getCollections(db?: string): Promise { const result = await this.bridge.sendCommand('getCollections', db ? { db } : {}) as { collections: ICollectionInfo[] }; return result.collections; } public async getDocuments( db: string, collection: string, limit = 50, skip = 0, ): Promise { return this.bridge.sendCommand('getDocuments', { db, collection, limit, skip }) as Promise; } }