2026-03-26 19:48:27 +00:00
|
|
|
import * as plugins from './plugins.js';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
import * as url from 'url';
|
|
|
|
|
import { EventEmitter } from 'events';
|
|
|
|
|
|
2026-04-02 17:02:03 +00:00
|
|
|
/**
|
|
|
|
|
* 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<string, any> | null;
|
|
|
|
|
previousDocument: Record<string, any> | 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<string, any>[];
|
|
|
|
|
total: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Server metrics.
|
|
|
|
|
*/
|
|
|
|
|
export interface ISmartDbMetrics {
|
|
|
|
|
databases: number;
|
|
|
|
|
collections: number;
|
|
|
|
|
oplogEntries: number;
|
|
|
|
|
oplogCurrentSeq: number;
|
|
|
|
|
uptimeSeconds: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 19:48:27 +00:00
|
|
|
/**
|
|
|
|
|
* Type-safe command definitions for the RustDb IPC protocol.
|
|
|
|
|
*/
|
|
|
|
|
type TSmartDbCommands = {
|
|
|
|
|
start: { params: { config: ISmartDbRustConfig }; result: { connectionUri: string } };
|
|
|
|
|
stop: { params: Record<string, never>; result: void };
|
|
|
|
|
getStatus: { params: Record<string, never>; result: { running: boolean } };
|
2026-04-02 17:02:03 +00:00
|
|
|
getMetrics: { params: Record<string, never>; result: ISmartDbMetrics };
|
|
|
|
|
getOpLog: {
|
|
|
|
|
params: { sinceSeq?: number; limit?: number; db?: string; collection?: string };
|
|
|
|
|
result: IOpLogResult;
|
|
|
|
|
};
|
|
|
|
|
getOpLogStats: { params: Record<string, never>; 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;
|
|
|
|
|
};
|
2026-03-26 19:48:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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<string, string> = { x64: 'amd64', arm64: 'arm64' };
|
|
|
|
|
const osMap: Record<string, string> = { 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<TSmartDbCommands>;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
|
|
this.bridge = new plugins.smartrust.RustBridge<TSmartDbCommands>({
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<void> {
|
|
|
|
|
await this.bridge.sendCommand('stop', {} as Record<string, never>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getStatus(): Promise<{ running: boolean }> {
|
|
|
|
|
return await this.bridge.sendCommand('getStatus', {} as Record<string, never>) as { running: boolean };
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 17:02:03 +00:00
|
|
|
public async getMetrics(): Promise<ISmartDbMetrics> {
|
|
|
|
|
return this.bridge.sendCommand('getMetrics', {} as Record<string, never>) as Promise<ISmartDbMetrics>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getOpLog(params: {
|
|
|
|
|
sinceSeq?: number;
|
|
|
|
|
limit?: number;
|
|
|
|
|
db?: string;
|
|
|
|
|
collection?: string;
|
|
|
|
|
} = {}): Promise<IOpLogResult> {
|
|
|
|
|
return this.bridge.sendCommand('getOpLog', params) as Promise<IOpLogResult>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getOpLogStats(): Promise<IOpLogStats> {
|
|
|
|
|
return this.bridge.sendCommand('getOpLogStats', {} as Record<string, never>) as Promise<IOpLogStats>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async revertToSeq(seq: number, dryRun = false): Promise<IRevertResult> {
|
|
|
|
|
return this.bridge.sendCommand('revertToSeq', { seq, dryRun }) as Promise<IRevertResult>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getCollections(db?: string): Promise<ICollectionInfo[]> {
|
|
|
|
|
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<IDocumentsResult> {
|
|
|
|
|
return this.bridge.sendCommand('getDocuments', { db, collection, limit, skip }) as Promise<IDocumentsResult>;
|
2026-03-26 19:48:27 +00:00
|
|
|
}
|
|
|
|
|
}
|