Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
/**
* Browser-specific entry point with IndexedDB support
*/
export * from './index';
import { RuDag } from './index';
/**
* Create a browser-optimized DAG with IndexedDB persistence
*/
export declare function createBrowserDag(name?: string): Promise<RuDag>;
/**
* Browser storage manager for DAGs
*/
export declare class BrowserDagManager {
private storage;
private initialized;
constructor();
init(): Promise<void>;
createDag(name?: string): Promise<RuDag>;
loadDag(id: string): Promise<RuDag | null>;
listDags(): Promise<import("./storage").StoredDag[]>;
deleteDag(id: string): Promise<boolean>;
clearAll(): Promise<void>;
getStats(): Promise<{
count: number;
totalSize: number;
}>;
close(): void;
}
//# sourceMappingURL=browser.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["browser.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,KAAK,EAAc,MAAM,SAAS,CAAC;AAE5C;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAKpE;AAED;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,WAAW,CAAS;;IAMtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAOxC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAK1C,QAAQ;IAKR,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKvC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAKzB,QAAQ;;;;IAKd,KAAK,IAAI,IAAI;CAId"}

View File

@@ -0,0 +1,80 @@
"use strict";
/**
* Browser-specific entry point with IndexedDB support
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserDagManager = void 0;
exports.createBrowserDag = createBrowserDag;
__exportStar(require("./index"), exports);
// Re-export with browser-specific defaults
const index_1 = require("./index");
/**
* Create a browser-optimized DAG with IndexedDB persistence
*/
async function createBrowserDag(name) {
const storage = new index_1.DagStorage();
const dag = new index_1.RuDag({ name, storage });
await dag.init();
return dag;
}
/**
* Browser storage manager for DAGs
*/
class BrowserDagManager {
constructor() {
this.initialized = false;
this.storage = new index_1.DagStorage();
}
async init() {
if (this.initialized)
return;
await this.storage.init();
this.initialized = true;
}
async createDag(name) {
await this.init();
const dag = new index_1.RuDag({ name, storage: this.storage });
await dag.init();
return dag;
}
async loadDag(id) {
await this.init();
return index_1.RuDag.load(id, this.storage);
}
async listDags() {
await this.init();
return this.storage.list();
}
async deleteDag(id) {
await this.init();
return this.storage.delete(id);
}
async clearAll() {
await this.init();
return this.storage.clear();
}
async getStats() {
await this.init();
return this.storage.stats();
}
close() {
this.storage.close();
this.initialized = false;
}
}
exports.BrowserDagManager = BrowserDagManager;
//# sourceMappingURL=browser.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"browser.js","sourceRoot":"","sources":["browser.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;;;;;;;;;;;;;AAUH,4CAKC;AAbD,0CAAwB;AAExB,2CAA2C;AAC3C,mCAA4C;AAE5C;;GAEG;AACI,KAAK,UAAU,gBAAgB,CAAC,IAAa;IAClD,MAAM,OAAO,GAAG,IAAI,kBAAU,EAAE,CAAC;IACjC,MAAM,GAAG,GAAG,IAAI,aAAK,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACzC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IACjB,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;GAEG;AACH,MAAa,iBAAiB;IAI5B;QAFQ,gBAAW,GAAG,KAAK,CAAC;QAG1B,IAAI,CAAC,OAAO,GAAG,IAAI,kBAAU,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,WAAW;YAAE,OAAO;QAC7B,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAC1B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,IAAa;QAC3B,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,GAAG,GAAG,IAAI,aAAK,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACvD,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU;QACtB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,aAAK,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,EAAU;QACxB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAC3B,CAAC;CACF;AAlDD,8CAkDC"}

View File

@@ -0,0 +1,73 @@
/**
* Browser-specific entry point with IndexedDB support
*/
export * from './index';
// Re-export with browser-specific defaults
import { RuDag, DagStorage } from './index';
/**
* Create a browser-optimized DAG with IndexedDB persistence
*/
export async function createBrowserDag(name?: string): Promise<RuDag> {
const storage = new DagStorage();
const dag = new RuDag({ name, storage });
await dag.init();
return dag;
}
/**
* Browser storage manager for DAGs
*/
export class BrowserDagManager {
private storage: DagStorage;
private initialized = false;
constructor() {
this.storage = new DagStorage();
}
async init(): Promise<void> {
if (this.initialized) return;
await this.storage.init();
this.initialized = true;
}
async createDag(name?: string): Promise<RuDag> {
await this.init();
const dag = new RuDag({ name, storage: this.storage });
await dag.init();
return dag;
}
async loadDag(id: string): Promise<RuDag | null> {
await this.init();
return RuDag.load(id, this.storage);
}
async listDags() {
await this.init();
return this.storage.list();
}
async deleteDag(id: string): Promise<boolean> {
await this.init();
return this.storage.delete(id);
}
async clearAll(): Promise<void> {
await this.init();
return this.storage.clear();
}
async getStats() {
await this.init();
return this.storage.stats();
}
close(): void {
this.storage.close();
this.initialized = false;
}
}

View File

@@ -0,0 +1,258 @@
/**
* High-level DAG API with WASM acceleration
* Provides a TypeScript-friendly interface to the WASM DAG implementation
*
* @security All inputs are validated to prevent injection attacks
* @performance Results are cached to minimize WASM calls
*/
import { DagStorage, MemoryStorage, StoredDag } from './storage';
/**
* Operator types for DAG nodes
*/
export declare enum DagOperator {
/** Table scan operation */
SCAN = 0,
/** Filter/WHERE clause */
FILTER = 1,
/** Column projection/SELECT */
PROJECT = 2,
/** Join operation */
JOIN = 3,
/** Aggregation (GROUP BY) */
AGGREGATE = 4,
/** Sort/ORDER BY */
SORT = 5,
/** Limit/TOP N */
LIMIT = 6,
/** Union of results */
UNION = 7,
/** Custom user-defined operator */
CUSTOM = 255
}
/**
* Attention mechanism types for node scoring
*/
export declare enum AttentionMechanism {
/** Score by position in topological order */
TOPOLOGICAL = 0,
/** Score by distance from critical path */
CRITICAL_PATH = 1,
/** Equal scores for all nodes */
UNIFORM = 2
}
/**
* Node representation in the DAG
*/
export interface DagNode {
/** Unique identifier for this node */
id: number;
/** The operator type (e.g., SCAN, FILTER, JOIN) */
operator: DagOperator | number;
/** Execution cost estimate for this node */
cost: number;
/** Optional arbitrary metadata attached to the node */
metadata?: Record<string, unknown>;
}
/**
* Edge representation (directed connection between nodes)
*/
export interface DagEdge {
/** Source node ID */
from: number;
/** Target node ID */
to: number;
}
/**
* Critical path result from DAG analysis
*/
export interface CriticalPath {
/** Node IDs in the critical path */
path: number[];
/** Total cost of the critical path */
cost: number;
}
/**
* DAG configuration options
*/
export interface RuDagOptions {
/** Custom ID for the DAG (auto-generated if not provided) */
id?: string;
/** Human-readable name */
name?: string;
/** Storage backend (IndexedDB/Memory/null for no persistence) */
storage?: DagStorage | MemoryStorage | null;
/** Auto-save changes to storage (default: true) */
autoSave?: boolean;
/** Error handler for background save failures */
onSaveError?: (error: unknown) => void;
}
/**
* RuDag - High-performance DAG with WASM acceleration and persistence
*
* @example
* ```typescript
* const dag = await new RuDag({ name: 'my-query' }).init();
* const scan = dag.addNode(DagOperator.SCAN, 10.0);
* const filter = dag.addNode(DagOperator.FILTER, 2.0);
* dag.addEdge(scan, filter);
* const { path, cost } = dag.criticalPath();
* ```
*/
export declare class RuDag {
private wasm;
private nodes;
private storage;
private readonly id;
private name?;
private autoSave;
private initialized;
private onSaveError?;
private _topoCache;
private _criticalPathCache;
private _dirty;
constructor(options?: RuDagOptions);
/**
* Initialize the DAG with WASM module and storage
* @returns This instance for chaining
* @throws {Error} If WASM module fails to load
* @throws {Error} If storage initialization fails
*/
init(): Promise<this>;
/**
* Ensure DAG is initialized
* @throws {Error} If DAG not initialized
*/
private ensureInit;
/**
* Handle background save errors
*/
private handleSaveError;
/**
* Invalidate caches (called when DAG structure changes)
*/
private invalidateCache;
/**
* Add a node to the DAG
* @param operator - The operator type
* @param cost - Execution cost estimate (must be non-negative)
* @param metadata - Optional metadata
* @returns The new node ID
* @throws {Error} If cost is invalid
*/
addNode(operator: DagOperator | number, cost: number, metadata?: Record<string, unknown>): number;
/**
* Add an edge between nodes
* @param from - Source node ID
* @param to - Target node ID
* @returns true if edge was added, false if it would create a cycle
* @throws {Error} If node IDs are invalid
*/
addEdge(from: number, to: number): boolean;
/**
* Get node count
*/
get nodeCount(): number;
/**
* Get edge count
*/
get edgeCount(): number;
/**
* Get topological sort (cached)
* @returns Array of node IDs in topological order
*/
topoSort(): number[];
/**
* Find critical path (cached)
* @returns Object with path (node IDs) and total cost
* @throws {Error} If WASM returns invalid data
*/
criticalPath(): CriticalPath;
/**
* Compute attention scores for nodes
* @param mechanism - Attention mechanism to use
* @returns Array of scores (one per node)
*/
attention(mechanism?: AttentionMechanism): number[];
/**
* Get node by ID
*/
getNode(id: number): DagNode | undefined;
/**
* Get all nodes
*/
getNodes(): DagNode[];
/**
* Serialize to bytes (bincode format)
*/
toBytes(): Uint8Array;
/**
* Serialize to JSON string
*/
toJSON(): string;
/**
* Save DAG to storage
* @returns StoredDag record or null if no storage configured
*/
save(): Promise<StoredDag | null>;
/**
* Load DAG from storage by ID
* @param id - DAG ID to load
* @param storage - Storage backend (creates default if not provided)
* @returns Loaded DAG or null if not found
* @throws {Error} If ID contains invalid characters
*/
static load(id: string, storage?: DagStorage | MemoryStorage): Promise<RuDag | null>;
/**
* Create DAG from bytes
* @param data - Serialized DAG data
* @param options - Configuration options
* @throws {Error} If data is empty or invalid
*/
static fromBytes(data: Uint8Array, options?: RuDagOptions): Promise<RuDag>;
/**
* Create DAG from JSON
* @param json - JSON string
* @param options - Configuration options
* @throws {Error} If JSON is empty or invalid
*/
static fromJSON(json: string, options?: RuDagOptions): Promise<RuDag>;
/**
* List all stored DAGs
* @param storage - Storage backend (creates default if not provided)
*/
static listStored(storage?: DagStorage | MemoryStorage): Promise<StoredDag[]>;
/**
* Delete a stored DAG
* @param id - DAG ID to delete
* @param storage - Storage backend (creates default if not provided)
* @throws {Error} If ID contains invalid characters
*/
static deleteStored(id: string, storage?: DagStorage | MemoryStorage): Promise<boolean>;
/**
* Get storage statistics
* @param storage - Storage backend (creates default if not provided)
*/
static storageStats(storage?: DagStorage | MemoryStorage): Promise<{
count: number;
totalSize: number;
}>;
/**
* Get DAG ID
*/
getId(): string;
/**
* Get DAG name
*/
getName(): string | undefined;
/**
* Set DAG name
* @param name - New name for the DAG
*/
setName(name: string): void;
/**
* Cleanup resources (WASM memory and storage connection)
* Always call this when done with a DAG to prevent memory leaks
*/
dispose(): void;
}
//# sourceMappingURL=dag.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"dag.d.ts","sourceRoot":"","sources":["dag.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,616 @@
"use strict";
/**
* High-level DAG API with WASM acceleration
* Provides a TypeScript-friendly interface to the WASM DAG implementation
*
* @security All inputs are validated to prevent injection attacks
* @performance Results are cached to minimize WASM calls
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.RuDag = exports.AttentionMechanism = exports.DagOperator = void 0;
const storage_1 = require("./storage");
/**
* Operator types for DAG nodes
*/
var DagOperator;
(function (DagOperator) {
/** Table scan operation */
DagOperator[DagOperator["SCAN"] = 0] = "SCAN";
/** Filter/WHERE clause */
DagOperator[DagOperator["FILTER"] = 1] = "FILTER";
/** Column projection/SELECT */
DagOperator[DagOperator["PROJECT"] = 2] = "PROJECT";
/** Join operation */
DagOperator[DagOperator["JOIN"] = 3] = "JOIN";
/** Aggregation (GROUP BY) */
DagOperator[DagOperator["AGGREGATE"] = 4] = "AGGREGATE";
/** Sort/ORDER BY */
DagOperator[DagOperator["SORT"] = 5] = "SORT";
/** Limit/TOP N */
DagOperator[DagOperator["LIMIT"] = 6] = "LIMIT";
/** Union of results */
DagOperator[DagOperator["UNION"] = 7] = "UNION";
/** Custom user-defined operator */
DagOperator[DagOperator["CUSTOM"] = 255] = "CUSTOM";
})(DagOperator || (exports.DagOperator = DagOperator = {}));
/**
* Attention mechanism types for node scoring
*/
var AttentionMechanism;
(function (AttentionMechanism) {
/** Score by position in topological order */
AttentionMechanism[AttentionMechanism["TOPOLOGICAL"] = 0] = "TOPOLOGICAL";
/** Score by distance from critical path */
AttentionMechanism[AttentionMechanism["CRITICAL_PATH"] = 1] = "CRITICAL_PATH";
/** Equal scores for all nodes */
AttentionMechanism[AttentionMechanism["UNIFORM"] = 2] = "UNIFORM";
})(AttentionMechanism || (exports.AttentionMechanism = AttentionMechanism = {}));
// WASM module singleton with loading promise for concurrent access
let wasmModule = null;
let wasmLoadPromise = null;
/**
* Initialize WASM module (singleton pattern with concurrent safety)
* @throws {Error} If WASM module fails to load
*/
async function initWasm() {
if (wasmModule)
return wasmModule;
// Prevent concurrent loading
if (wasmLoadPromise)
return wasmLoadPromise;
wasmLoadPromise = (async () => {
try {
// Try browser bundler version first
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mod = await Promise.resolve().then(() => __importStar(require('../pkg/ruvector_dag_wasm.js')));
if (typeof mod.default === 'function') {
await mod.default();
}
wasmModule = mod;
return wasmModule;
}
catch {
try {
// Fallback to Node.js version
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mod = await Promise.resolve().then(() => __importStar(require('../pkg-node/ruvector_dag_wasm.js')));
wasmModule = mod;
return wasmModule;
}
catch (e) {
wasmLoadPromise = null; // Allow retry on failure
throw new Error(`Failed to load WASM module: ${e}`);
}
}
})();
return wasmLoadPromise;
}
/**
* Type guard for CriticalPath validation
* @security Prevents prototype pollution from untrusted WASM output
*/
function isCriticalPath(obj) {
if (typeof obj !== 'object' || obj === null)
return false;
if (Object.getPrototypeOf(obj) !== Object.prototype && Object.getPrototypeOf(obj) !== null)
return false;
const candidate = obj;
if (!('path' in candidate) || !Array.isArray(candidate.path))
return false;
if (!candidate.path.every((item) => typeof item === 'number' && Number.isFinite(item)))
return false;
if (!('cost' in candidate) || typeof candidate.cost !== 'number')
return false;
if (!Number.isFinite(candidate.cost))
return false;
return true;
}
/**
* Validate DAG ID to prevent injection attacks
* @security Prevents path traversal and special character injection
*/
function isValidDagId(id) {
if (typeof id !== 'string' || id.length === 0 || id.length > 256)
return false;
// Only allow alphanumeric, dash, underscore
return /^[a-zA-Z0-9_-]+$/.test(id);
}
/**
* Sanitize ID or generate a safe one
*/
function sanitizeOrGenerateId(id) {
if (id && isValidDagId(id))
return id;
// Generate safe ID
const timestamp = Date.now();
const random = Math.random().toString(36).slice(2, 8);
return `dag-${timestamp}-${random}`;
}
/**
* RuDag - High-performance DAG with WASM acceleration and persistence
*
* @example
* ```typescript
* const dag = await new RuDag({ name: 'my-query' }).init();
* const scan = dag.addNode(DagOperator.SCAN, 10.0);
* const filter = dag.addNode(DagOperator.FILTER, 2.0);
* dag.addEdge(scan, filter);
* const { path, cost } = dag.criticalPath();
* ```
*/
class RuDag {
constructor(options = {}) {
this.wasm = null;
this.nodes = new Map();
this.initialized = false;
// Cache for expensive operations
this._topoCache = null;
this._criticalPathCache = null;
this._dirty = true;
this.id = sanitizeOrGenerateId(options.id);
this.name = options.name;
this.storage = options.storage === undefined ? (0, storage_1.createStorage)() : options.storage;
this.autoSave = options.autoSave ?? true;
this.onSaveError = options.onSaveError;
}
/**
* Initialize the DAG with WASM module and storage
* @returns This instance for chaining
* @throws {Error} If WASM module fails to load
* @throws {Error} If storage initialization fails
*/
async init() {
if (this.initialized)
return this;
const mod = await initWasm();
try {
this.wasm = new mod.WasmDag();
}
catch (error) {
throw new Error(`Failed to create WASM DAG instance: ${error}`);
}
try {
if (this.storage) {
await this.storage.init();
}
}
catch (error) {
// Cleanup WASM on storage failure
if (this.wasm) {
this.wasm.free();
this.wasm = null;
}
throw new Error(`Failed to initialize storage: ${error}`);
}
this.initialized = true;
return this;
}
/**
* Ensure DAG is initialized
* @throws {Error} If DAG not initialized
*/
ensureInit() {
if (!this.wasm) {
throw new Error('DAG not initialized. Call init() first.');
}
return this.wasm;
}
/**
* Handle background save errors
*/
handleSaveError(error) {
if (this.onSaveError) {
this.onSaveError(error);
}
else {
console.warn('[RuDag] Background save failed:', error);
}
}
/**
* Invalidate caches (called when DAG structure changes)
*/
invalidateCache() {
this._dirty = true;
this._topoCache = null;
this._criticalPathCache = null;
}
/**
* Add a node to the DAG
* @param operator - The operator type
* @param cost - Execution cost estimate (must be non-negative)
* @param metadata - Optional metadata
* @returns The new node ID
* @throws {Error} If cost is invalid
*/
addNode(operator, cost, metadata) {
// Input validation
if (!Number.isFinite(cost) || cost < 0) {
throw new Error(`Invalid cost: ${cost}. Must be a non-negative finite number.`);
}
if (!Number.isInteger(operator) || operator < 0 || operator > 255) {
throw new Error(`Invalid operator: ${operator}. Must be an integer 0-255.`);
}
const wasm = this.ensureInit();
const id = wasm.add_node(operator, cost);
this.nodes.set(id, {
id,
operator,
cost,
metadata,
});
this.invalidateCache();
if (this.autoSave) {
this.save().catch((e) => this.handleSaveError(e));
}
return id;
}
/**
* Add an edge between nodes
* @param from - Source node ID
* @param to - Target node ID
* @returns true if edge was added, false if it would create a cycle
* @throws {Error} If node IDs are invalid
*/
addEdge(from, to) {
// Input validation
if (!Number.isInteger(from) || from < 0) {
throw new Error(`Invalid 'from' node ID: ${from}`);
}
if (!Number.isInteger(to) || to < 0) {
throw new Error(`Invalid 'to' node ID: ${to}`);
}
if (from === to) {
throw new Error('Self-loops are not allowed in a DAG');
}
const wasm = this.ensureInit();
const success = wasm.add_edge(from, to);
if (success) {
this.invalidateCache();
if (this.autoSave) {
this.save().catch((e) => this.handleSaveError(e));
}
}
return success;
}
/**
* Get node count
*/
get nodeCount() {
return this.ensureInit().node_count();
}
/**
* Get edge count
*/
get edgeCount() {
return this.ensureInit().edge_count();
}
/**
* Get topological sort (cached)
* @returns Array of node IDs in topological order
*/
topoSort() {
if (!this._dirty && this._topoCache) {
return [...this._topoCache]; // Return copy to prevent mutation
}
const result = this.ensureInit().topo_sort();
this._topoCache = Array.from(result);
return [...this._topoCache];
}
/**
* Find critical path (cached)
* @returns Object with path (node IDs) and total cost
* @throws {Error} If WASM returns invalid data
*/
criticalPath() {
if (!this._dirty && this._criticalPathCache) {
return { ...this._criticalPathCache, path: [...this._criticalPathCache.path] };
}
const result = this.ensureInit().critical_path();
let parsed;
if (typeof result === 'string') {
try {
parsed = JSON.parse(result);
}
catch (e) {
throw new Error(`Invalid critical path JSON from WASM: ${e}`);
}
}
else {
parsed = result;
}
if (!isCriticalPath(parsed)) {
throw new Error('Invalid critical path structure from WASM');
}
this._criticalPathCache = parsed;
this._dirty = false;
return { ...parsed, path: [...parsed.path] };
}
/**
* Compute attention scores for nodes
* @param mechanism - Attention mechanism to use
* @returns Array of scores (one per node)
*/
attention(mechanism = AttentionMechanism.CRITICAL_PATH) {
if (!Number.isInteger(mechanism) || mechanism < 0 || mechanism > 2) {
throw new Error(`Invalid attention mechanism: ${mechanism}`);
}
const result = this.ensureInit().attention(mechanism);
return Array.from(result);
}
/**
* Get node by ID
*/
getNode(id) {
return this.nodes.get(id);
}
/**
* Get all nodes
*/
getNodes() {
return Array.from(this.nodes.values());
}
/**
* Serialize to bytes (bincode format)
*/
toBytes() {
return this.ensureInit().to_bytes();
}
/**
* Serialize to JSON string
*/
toJSON() {
return this.ensureInit().to_json();
}
/**
* Save DAG to storage
* @returns StoredDag record or null if no storage configured
*/
async save() {
if (!this.storage)
return null;
const data = this.toBytes();
return this.storage.save(this.id, data, {
name: this.name,
metadata: {
nodeCount: this.nodeCount,
edgeCount: this.edgeCount,
nodes: Object.fromEntries(this.nodes),
},
});
}
/**
* Load DAG from storage by ID
* @param id - DAG ID to load
* @param storage - Storage backend (creates default if not provided)
* @returns Loaded DAG or null if not found
* @throws {Error} If ID contains invalid characters
*/
static async load(id, storage) {
if (!isValidDagId(id)) {
throw new Error(`Invalid DAG ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const isOwnedStorage = !storage;
const store = storage || (0, storage_1.createStorage)();
try {
await store.init();
const record = await store.get(id);
if (!record) {
if (isOwnedStorage)
store.close();
return null;
}
return RuDag.fromBytes(record.data, {
id: record.id,
name: record.name,
storage: store,
});
}
catch (error) {
if (isOwnedStorage)
store.close();
throw error;
}
}
/**
* Create DAG from bytes
* @param data - Serialized DAG data
* @param options - Configuration options
* @throws {Error} If data is empty or invalid
*/
static async fromBytes(data, options = {}) {
if (!data || data.length === 0) {
throw new Error('Cannot create DAG from empty or null data');
}
const mod = await initWasm();
const dag = new RuDag(options);
try {
dag.wasm = mod.WasmDag.from_bytes(data);
}
catch (error) {
throw new Error(`Failed to deserialize DAG from bytes: ${error}`);
}
dag.initialized = true;
if (dag.storage) {
try {
await dag.storage.init();
}
catch (error) {
dag.wasm?.free();
dag.wasm = null;
throw new Error(`Failed to initialize storage: ${error}`);
}
}
return dag;
}
/**
* Create DAG from JSON
* @param json - JSON string
* @param options - Configuration options
* @throws {Error} If JSON is empty or invalid
*/
static async fromJSON(json, options = {}) {
if (!json || json.trim().length === 0) {
throw new Error('Cannot create DAG from empty or null JSON');
}
const mod = await initWasm();
const dag = new RuDag(options);
try {
dag.wasm = mod.WasmDag.from_json(json);
}
catch (error) {
throw new Error(`Failed to deserialize DAG from JSON: ${error}`);
}
dag.initialized = true;
if (dag.storage) {
try {
await dag.storage.init();
}
catch (error) {
dag.wasm?.free();
dag.wasm = null;
throw new Error(`Failed to initialize storage: ${error}`);
}
}
return dag;
}
/**
* List all stored DAGs
* @param storage - Storage backend (creates default if not provided)
*/
static async listStored(storage) {
const isOwnedStorage = !storage;
const store = storage || (0, storage_1.createStorage)();
try {
await store.init();
const result = await store.list();
if (isOwnedStorage)
store.close();
return result;
}
catch (error) {
if (isOwnedStorage)
store.close();
throw error;
}
}
/**
* Delete a stored DAG
* @param id - DAG ID to delete
* @param storage - Storage backend (creates default if not provided)
* @throws {Error} If ID contains invalid characters
*/
static async deleteStored(id, storage) {
if (!isValidDagId(id)) {
throw new Error(`Invalid DAG ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const isOwnedStorage = !storage;
const store = storage || (0, storage_1.createStorage)();
try {
await store.init();
const result = await store.delete(id);
if (isOwnedStorage)
store.close();
return result;
}
catch (error) {
if (isOwnedStorage)
store.close();
throw error;
}
}
/**
* Get storage statistics
* @param storage - Storage backend (creates default if not provided)
*/
static async storageStats(storage) {
const isOwnedStorage = !storage;
const store = storage || (0, storage_1.createStorage)();
try {
await store.init();
const result = await store.stats();
if (isOwnedStorage)
store.close();
return result;
}
catch (error) {
if (isOwnedStorage)
store.close();
throw error;
}
}
/**
* Get DAG ID
*/
getId() {
return this.id;
}
/**
* Get DAG name
*/
getName() {
return this.name;
}
/**
* Set DAG name
* @param name - New name for the DAG
*/
setName(name) {
this.name = name;
if (this.autoSave) {
this.save().catch((e) => this.handleSaveError(e));
}
}
/**
* Cleanup resources (WASM memory and storage connection)
* Always call this when done with a DAG to prevent memory leaks
*/
dispose() {
if (this.wasm) {
this.wasm.free();
this.wasm = null;
}
if (this.storage) {
this.storage.close();
this.storage = null;
}
this.nodes.clear();
this._topoCache = null;
this._criticalPathCache = null;
this.initialized = false;
}
}
exports.RuDag = RuDag;
//# sourceMappingURL=dag.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,699 @@
/**
* High-level DAG API with WASM acceleration
* Provides a TypeScript-friendly interface to the WASM DAG implementation
*
* @security All inputs are validated to prevent injection attacks
* @performance Results are cached to minimize WASM calls
*/
import { createStorage, DagStorage, MemoryStorage, StoredDag } from './storage';
// WASM module type definitions
interface WasmDagModule {
WasmDag: {
new(): WasmDagInstance;
from_bytes(data: Uint8Array): WasmDagInstance;
from_json(json: string): WasmDagInstance;
};
}
interface WasmDagInstance {
add_node(op: number, cost: number): number;
add_edge(from: number, to: number): boolean;
node_count(): number;
edge_count(): number;
topo_sort(): Uint32Array;
critical_path(): string | CriticalPath;
attention(mechanism: number): Float32Array;
to_bytes(): Uint8Array;
to_json(): string;
free(): void;
}
/**
* Operator types for DAG nodes
*/
export enum DagOperator {
/** Table scan operation */
SCAN = 0,
/** Filter/WHERE clause */
FILTER = 1,
/** Column projection/SELECT */
PROJECT = 2,
/** Join operation */
JOIN = 3,
/** Aggregation (GROUP BY) */
AGGREGATE = 4,
/** Sort/ORDER BY */
SORT = 5,
/** Limit/TOP N */
LIMIT = 6,
/** Union of results */
UNION = 7,
/** Custom user-defined operator */
CUSTOM = 255,
}
/**
* Attention mechanism types for node scoring
*/
export enum AttentionMechanism {
/** Score by position in topological order */
TOPOLOGICAL = 0,
/** Score by distance from critical path */
CRITICAL_PATH = 1,
/** Equal scores for all nodes */
UNIFORM = 2,
}
/**
* Node representation in the DAG
*/
export interface DagNode {
/** Unique identifier for this node */
id: number;
/** The operator type (e.g., SCAN, FILTER, JOIN) */
operator: DagOperator | number;
/** Execution cost estimate for this node */
cost: number;
/** Optional arbitrary metadata attached to the node */
metadata?: Record<string, unknown>;
}
/**
* Edge representation (directed connection between nodes)
*/
export interface DagEdge {
/** Source node ID */
from: number;
/** Target node ID */
to: number;
}
/**
* Critical path result from DAG analysis
*/
export interface CriticalPath {
/** Node IDs in the critical path */
path: number[];
/** Total cost of the critical path */
cost: number;
}
/**
* DAG configuration options
*/
export interface RuDagOptions {
/** Custom ID for the DAG (auto-generated if not provided) */
id?: string;
/** Human-readable name */
name?: string;
/** Storage backend (IndexedDB/Memory/null for no persistence) */
storage?: DagStorage | MemoryStorage | null;
/** Auto-save changes to storage (default: true) */
autoSave?: boolean;
/** Error handler for background save failures */
onSaveError?: (error: unknown) => void;
}
// WASM module singleton with loading promise for concurrent access
let wasmModule: WasmDagModule | null = null;
let wasmLoadPromise: Promise<WasmDagModule> | null = null;
/**
* Initialize WASM module (singleton pattern with concurrent safety)
* @throws {Error} If WASM module fails to load
*/
async function initWasm(): Promise<WasmDagModule> {
if (wasmModule) return wasmModule;
// Prevent concurrent loading
if (wasmLoadPromise) return wasmLoadPromise;
wasmLoadPromise = (async () => {
try {
// Try browser bundler version first
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mod = await import('../pkg/ruvector_dag_wasm.js') as any;
if (typeof mod.default === 'function') {
await mod.default();
}
wasmModule = mod as WasmDagModule;
return wasmModule;
} catch {
try {
// Fallback to Node.js version
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mod = await import('../pkg-node/ruvector_dag_wasm.js') as any;
wasmModule = mod as WasmDagModule;
return wasmModule;
} catch (e) {
wasmLoadPromise = null; // Allow retry on failure
throw new Error(`Failed to load WASM module: ${e}`);
}
}
})();
return wasmLoadPromise;
}
/**
* Type guard for CriticalPath validation
* @security Prevents prototype pollution from untrusted WASM output
*/
function isCriticalPath(obj: unknown): obj is CriticalPath {
if (typeof obj !== 'object' || obj === null) return false;
if (Object.getPrototypeOf(obj) !== Object.prototype && Object.getPrototypeOf(obj) !== null) return false;
const candidate = obj as Record<string, unknown>;
if (!('path' in candidate) || !Array.isArray(candidate.path)) return false;
if (!candidate.path.every((item: unknown) => typeof item === 'number' && Number.isFinite(item))) return false;
if (!('cost' in candidate) || typeof candidate.cost !== 'number') return false;
if (!Number.isFinite(candidate.cost)) return false;
return true;
}
/**
* Validate DAG ID to prevent injection attacks
* @security Prevents path traversal and special character injection
*/
function isValidDagId(id: string): boolean {
if (typeof id !== 'string' || id.length === 0 || id.length > 256) return false;
// Only allow alphanumeric, dash, underscore
return /^[a-zA-Z0-9_-]+$/.test(id);
}
/**
* Sanitize ID or generate a safe one
*/
function sanitizeOrGenerateId(id?: string): string {
if (id && isValidDagId(id)) return id;
// Generate safe ID
const timestamp = Date.now();
const random = Math.random().toString(36).slice(2, 8);
return `dag-${timestamp}-${random}`;
}
/**
* RuDag - High-performance DAG with WASM acceleration and persistence
*
* @example
* ```typescript
* const dag = await new RuDag({ name: 'my-query' }).init();
* const scan = dag.addNode(DagOperator.SCAN, 10.0);
* const filter = dag.addNode(DagOperator.FILTER, 2.0);
* dag.addEdge(scan, filter);
* const { path, cost } = dag.criticalPath();
* ```
*/
export class RuDag {
private wasm: WasmDagInstance | null = null;
private nodes: Map<number, DagNode> = new Map();
private storage: DagStorage | MemoryStorage | null;
private readonly id: string;
private name?: string;
private autoSave: boolean;
private initialized = false;
private onSaveError?: (error: unknown) => void;
// Cache for expensive operations
private _topoCache: number[] | null = null;
private _criticalPathCache: CriticalPath | null = null;
private _dirty = true;
constructor(options: RuDagOptions = {}) {
this.id = sanitizeOrGenerateId(options.id);
this.name = options.name;
this.storage = options.storage === undefined ? createStorage() : options.storage;
this.autoSave = options.autoSave ?? true;
this.onSaveError = options.onSaveError;
}
/**
* Initialize the DAG with WASM module and storage
* @returns This instance for chaining
* @throws {Error} If WASM module fails to load
* @throws {Error} If storage initialization fails
*/
async init(): Promise<this> {
if (this.initialized) return this;
const mod = await initWasm();
try {
this.wasm = new mod.WasmDag();
} catch (error) {
throw new Error(`Failed to create WASM DAG instance: ${error}`);
}
try {
if (this.storage) {
await this.storage.init();
}
} catch (error) {
// Cleanup WASM on storage failure
if (this.wasm) {
this.wasm.free();
this.wasm = null;
}
throw new Error(`Failed to initialize storage: ${error}`);
}
this.initialized = true;
return this;
}
/**
* Ensure DAG is initialized
* @throws {Error} If DAG not initialized
*/
private ensureInit(): WasmDagInstance {
if (!this.wasm) {
throw new Error('DAG not initialized. Call init() first.');
}
return this.wasm;
}
/**
* Handle background save errors
*/
private handleSaveError(error: unknown): void {
if (this.onSaveError) {
this.onSaveError(error);
} else {
console.warn('[RuDag] Background save failed:', error);
}
}
/**
* Invalidate caches (called when DAG structure changes)
*/
private invalidateCache(): void {
this._dirty = true;
this._topoCache = null;
this._criticalPathCache = null;
}
/**
* Add a node to the DAG
* @param operator - The operator type
* @param cost - Execution cost estimate (must be non-negative)
* @param metadata - Optional metadata
* @returns The new node ID
* @throws {Error} If cost is invalid
*/
addNode(operator: DagOperator | number, cost: number, metadata?: Record<string, unknown>): number {
// Input validation
if (!Number.isFinite(cost) || cost < 0) {
throw new Error(`Invalid cost: ${cost}. Must be a non-negative finite number.`);
}
if (!Number.isInteger(operator) || operator < 0 || operator > 255) {
throw new Error(`Invalid operator: ${operator}. Must be an integer 0-255.`);
}
const wasm = this.ensureInit();
const id = wasm.add_node(operator, cost);
this.nodes.set(id, {
id,
operator,
cost,
metadata,
});
this.invalidateCache();
if (this.autoSave) {
this.save().catch((e) => this.handleSaveError(e));
}
return id;
}
/**
* Add an edge between nodes
* @param from - Source node ID
* @param to - Target node ID
* @returns true if edge was added, false if it would create a cycle
* @throws {Error} If node IDs are invalid
*/
addEdge(from: number, to: number): boolean {
// Input validation
if (!Number.isInteger(from) || from < 0) {
throw new Error(`Invalid 'from' node ID: ${from}`);
}
if (!Number.isInteger(to) || to < 0) {
throw new Error(`Invalid 'to' node ID: ${to}`);
}
if (from === to) {
throw new Error('Self-loops are not allowed in a DAG');
}
const wasm = this.ensureInit();
const success = wasm.add_edge(from, to);
if (success) {
this.invalidateCache();
if (this.autoSave) {
this.save().catch((e) => this.handleSaveError(e));
}
}
return success;
}
/**
* Get node count
*/
get nodeCount(): number {
return this.ensureInit().node_count();
}
/**
* Get edge count
*/
get edgeCount(): number {
return this.ensureInit().edge_count();
}
/**
* Get topological sort (cached)
* @returns Array of node IDs in topological order
*/
topoSort(): number[] {
if (!this._dirty && this._topoCache) {
return [...this._topoCache]; // Return copy to prevent mutation
}
const result = this.ensureInit().topo_sort();
this._topoCache = Array.from(result);
return [...this._topoCache];
}
/**
* Find critical path (cached)
* @returns Object with path (node IDs) and total cost
* @throws {Error} If WASM returns invalid data
*/
criticalPath(): CriticalPath {
if (!this._dirty && this._criticalPathCache) {
return { ...this._criticalPathCache, path: [...this._criticalPathCache.path] };
}
const result = this.ensureInit().critical_path();
let parsed: unknown;
if (typeof result === 'string') {
try {
parsed = JSON.parse(result);
} catch (e) {
throw new Error(`Invalid critical path JSON from WASM: ${e}`);
}
} else {
parsed = result;
}
if (!isCriticalPath(parsed)) {
throw new Error('Invalid critical path structure from WASM');
}
this._criticalPathCache = parsed;
this._dirty = false;
return { ...parsed, path: [...parsed.path] };
}
/**
* Compute attention scores for nodes
* @param mechanism - Attention mechanism to use
* @returns Array of scores (one per node)
*/
attention(mechanism: AttentionMechanism = AttentionMechanism.CRITICAL_PATH): number[] {
if (!Number.isInteger(mechanism) || mechanism < 0 || mechanism > 2) {
throw new Error(`Invalid attention mechanism: ${mechanism}`);
}
const result = this.ensureInit().attention(mechanism);
return Array.from(result);
}
/**
* Get node by ID
*/
getNode(id: number): DagNode | undefined {
return this.nodes.get(id);
}
/**
* Get all nodes
*/
getNodes(): DagNode[] {
return Array.from(this.nodes.values());
}
/**
* Serialize to bytes (bincode format)
*/
toBytes(): Uint8Array {
return this.ensureInit().to_bytes();
}
/**
* Serialize to JSON string
*/
toJSON(): string {
return this.ensureInit().to_json();
}
/**
* Save DAG to storage
* @returns StoredDag record or null if no storage configured
*/
async save(): Promise<StoredDag | null> {
if (!this.storage) return null;
const data = this.toBytes();
return this.storage.save(this.id, data, {
name: this.name,
metadata: {
nodeCount: this.nodeCount,
edgeCount: this.edgeCount,
nodes: Object.fromEntries(this.nodes),
},
});
}
/**
* Load DAG from storage by ID
* @param id - DAG ID to load
* @param storage - Storage backend (creates default if not provided)
* @returns Loaded DAG or null if not found
* @throws {Error} If ID contains invalid characters
*/
static async load(id: string, storage?: DagStorage | MemoryStorage): Promise<RuDag | null> {
if (!isValidDagId(id)) {
throw new Error(`Invalid DAG ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const isOwnedStorage = !storage;
const store = storage || createStorage();
try {
await store.init();
const record = await store.get(id);
if (!record) {
if (isOwnedStorage) store.close();
return null;
}
return RuDag.fromBytes(record.data, {
id: record.id,
name: record.name,
storage: store,
});
} catch (error) {
if (isOwnedStorage) store.close();
throw error;
}
}
/**
* Create DAG from bytes
* @param data - Serialized DAG data
* @param options - Configuration options
* @throws {Error} If data is empty or invalid
*/
static async fromBytes(data: Uint8Array, options: RuDagOptions = {}): Promise<RuDag> {
if (!data || data.length === 0) {
throw new Error('Cannot create DAG from empty or null data');
}
const mod = await initWasm();
const dag = new RuDag(options);
try {
dag.wasm = mod.WasmDag.from_bytes(data);
} catch (error) {
throw new Error(`Failed to deserialize DAG from bytes: ${error}`);
}
dag.initialized = true;
if (dag.storage) {
try {
await dag.storage.init();
} catch (error) {
dag.wasm?.free();
dag.wasm = null;
throw new Error(`Failed to initialize storage: ${error}`);
}
}
return dag;
}
/**
* Create DAG from JSON
* @param json - JSON string
* @param options - Configuration options
* @throws {Error} If JSON is empty or invalid
*/
static async fromJSON(json: string, options: RuDagOptions = {}): Promise<RuDag> {
if (!json || json.trim().length === 0) {
throw new Error('Cannot create DAG from empty or null JSON');
}
const mod = await initWasm();
const dag = new RuDag(options);
try {
dag.wasm = mod.WasmDag.from_json(json);
} catch (error) {
throw new Error(`Failed to deserialize DAG from JSON: ${error}`);
}
dag.initialized = true;
if (dag.storage) {
try {
await dag.storage.init();
} catch (error) {
dag.wasm?.free();
dag.wasm = null;
throw new Error(`Failed to initialize storage: ${error}`);
}
}
return dag;
}
/**
* List all stored DAGs
* @param storage - Storage backend (creates default if not provided)
*/
static async listStored(storage?: DagStorage | MemoryStorage): Promise<StoredDag[]> {
const isOwnedStorage = !storage;
const store = storage || createStorage();
try {
await store.init();
const result = await store.list();
if (isOwnedStorage) store.close();
return result;
} catch (error) {
if (isOwnedStorage) store.close();
throw error;
}
}
/**
* Delete a stored DAG
* @param id - DAG ID to delete
* @param storage - Storage backend (creates default if not provided)
* @throws {Error} If ID contains invalid characters
*/
static async deleteStored(id: string, storage?: DagStorage | MemoryStorage): Promise<boolean> {
if (!isValidDagId(id)) {
throw new Error(`Invalid DAG ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const isOwnedStorage = !storage;
const store = storage || createStorage();
try {
await store.init();
const result = await store.delete(id);
if (isOwnedStorage) store.close();
return result;
} catch (error) {
if (isOwnedStorage) store.close();
throw error;
}
}
/**
* Get storage statistics
* @param storage - Storage backend (creates default if not provided)
*/
static async storageStats(storage?: DagStorage | MemoryStorage): Promise<{ count: number; totalSize: number }> {
const isOwnedStorage = !storage;
const store = storage || createStorage();
try {
await store.init();
const result = await store.stats();
if (isOwnedStorage) store.close();
return result;
} catch (error) {
if (isOwnedStorage) store.close();
throw error;
}
}
/**
* Get DAG ID
*/
getId(): string {
return this.id;
}
/**
* Get DAG name
*/
getName(): string | undefined {
return this.name;
}
/**
* Set DAG name
* @param name - New name for the DAG
*/
setName(name: string): void {
this.name = name;
if (this.autoSave) {
this.save().catch((e) => this.handleSaveError(e));
}
}
/**
* Cleanup resources (WASM memory and storage connection)
* Always call this when done with a DAG to prevent memory leaks
*/
dispose(): void {
if (this.wasm) {
this.wasm.free();
this.wasm = null;
}
if (this.storage) {
this.storage.close();
this.storage = null;
}
this.nodes.clear();
this._topoCache = null;
this._criticalPathCache = null;
this.initialized = false;
}
}

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,EACL,WAAW,EACX,kBAAkB,EAClB,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,oBAAoB,EACpB,KAAK,SAAS,EACd,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC;AAGnB,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG"}

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,6BAQe;AAPb,4FAAA,KAAK,OAAA;AACL,kGAAA,WAAW,OAAA;AACX,yGAAA,kBAAkB,OAAA;AAOpB,qCAOmB;AANjB,qGAAA,UAAU,OAAA;AACV,wGAAA,aAAa,OAAA;AACb,wGAAA,aAAa,OAAA;AACb,+GAAA,oBAAoB,OAAA;AAKtB,eAAe;AACF,QAAA,OAAO,GAAG,OAAO,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG"}

View File

@@ -0,0 +1,216 @@
/**
* Tests for @ruvector/rudag
*/
import { test, describe, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';
import { RuDag, DagOperator, AttentionMechanism, MemoryStorage, createStorage } from './index';
describe('RuDag', () => {
let dag: RuDag;
beforeEach(async () => {
dag = new RuDag({ storage: new MemoryStorage(), autoSave: false });
await dag.init();
});
afterEach(() => {
dag.dispose();
});
test('should create empty DAG', () => {
assert.strictEqual(dag.nodeCount, 0);
assert.strictEqual(dag.edgeCount, 0);
});
test('should add nodes', () => {
const id1 = dag.addNode(DagOperator.SCAN, 10.0);
const id2 = dag.addNode(DagOperator.FILTER, 2.0);
assert.strictEqual(id1, 0);
assert.strictEqual(id2, 1);
assert.strictEqual(dag.nodeCount, 2);
});
test('should add edges', () => {
const n1 = dag.addNode(DagOperator.SCAN, 10.0);
const n2 = dag.addNode(DagOperator.FILTER, 2.0);
const success = dag.addEdge(n1, n2);
assert.strictEqual(success, true);
assert.strictEqual(dag.edgeCount, 1);
});
test('should reject cycles', () => {
const n1 = dag.addNode(DagOperator.SCAN, 1.0);
const n2 = dag.addNode(DagOperator.FILTER, 1.0);
const n3 = dag.addNode(DagOperator.PROJECT, 1.0);
dag.addEdge(n1, n2);
dag.addEdge(n2, n3);
// This should fail - would create cycle
const success = dag.addEdge(n3, n1);
assert.strictEqual(success, false);
});
test('should compute topological sort', () => {
const n1 = dag.addNode(DagOperator.SCAN, 1.0);
const n2 = dag.addNode(DagOperator.FILTER, 1.0);
const n3 = dag.addNode(DagOperator.PROJECT, 1.0);
dag.addEdge(n1, n2);
dag.addEdge(n2, n3);
const topo = dag.topoSort();
assert.deepStrictEqual(topo, [0, 1, 2]);
});
test('should find critical path', () => {
const n1 = dag.addNode(DagOperator.SCAN, 10.0);
const n2 = dag.addNode(DagOperator.FILTER, 2.0);
const n3 = dag.addNode(DagOperator.PROJECT, 1.0);
dag.addEdge(n1, n2);
dag.addEdge(n2, n3);
const result = dag.criticalPath();
assert.deepStrictEqual(result.path, [0, 1, 2]);
assert.strictEqual(result.cost, 13); // 10 + 2 + 1
});
test('should compute attention scores', () => {
dag.addNode(DagOperator.SCAN, 1.0);
dag.addNode(DagOperator.FILTER, 2.0);
dag.addNode(DagOperator.PROJECT, 3.0);
const uniform = dag.attention(AttentionMechanism.UNIFORM);
assert.strictEqual(uniform.length, 3);
// All should be approximately 0.333
assert.ok(Math.abs(uniform[0] - 0.333) < 0.01);
const topo = dag.attention(AttentionMechanism.TOPOLOGICAL);
assert.strictEqual(topo.length, 3);
const critical = dag.attention(AttentionMechanism.CRITICAL_PATH);
assert.strictEqual(critical.length, 3);
});
test('should serialize to JSON', () => {
dag.addNode(DagOperator.SCAN, 1.0);
dag.addNode(DagOperator.FILTER, 2.0);
dag.addEdge(0, 1);
const json = dag.toJSON();
assert.ok(json.includes('nodes'));
assert.ok(json.includes('edges'));
});
test('should serialize to bytes', () => {
dag.addNode(DagOperator.SCAN, 1.0);
dag.addNode(DagOperator.FILTER, 2.0);
dag.addEdge(0, 1);
const bytes = dag.toBytes();
assert.ok(bytes instanceof Uint8Array);
assert.ok(bytes.length > 0);
});
test('should round-trip through JSON', async () => {
const n1 = dag.addNode(DagOperator.SCAN, 10.0);
const n2 = dag.addNode(DagOperator.FILTER, 2.0);
dag.addEdge(n1, n2);
const json = dag.toJSON();
const restored = await RuDag.fromJSON(json, { storage: null });
assert.strictEqual(restored.nodeCount, 2);
assert.strictEqual(restored.edgeCount, 1);
restored.dispose();
});
test('should round-trip through bytes', async () => {
const n1 = dag.addNode(DagOperator.SCAN, 10.0);
const n2 = dag.addNode(DagOperator.FILTER, 2.0);
dag.addEdge(n1, n2);
const bytes = dag.toBytes();
const restored = await RuDag.fromBytes(bytes, { storage: null });
assert.strictEqual(restored.nodeCount, 2);
assert.strictEqual(restored.edgeCount, 1);
restored.dispose();
});
});
describe('MemoryStorage', () => {
let storage: MemoryStorage;
beforeEach(async () => {
storage = new MemoryStorage();
await storage.init();
});
test('should save and retrieve DAG', async () => {
const data = new Uint8Array([1, 2, 3, 4]);
await storage.save('test-dag', data, { name: 'Test DAG' });
const retrieved = await storage.get('test-dag');
assert.ok(retrieved);
assert.strictEqual(retrieved.id, 'test-dag');
assert.strictEqual(retrieved.name, 'Test DAG');
assert.deepStrictEqual(Array.from(retrieved.data), [1, 2, 3, 4]);
});
test('should list all DAGs', async () => {
await storage.save('dag-1', new Uint8Array([1]));
await storage.save('dag-2', new Uint8Array([2]));
const list = await storage.list();
assert.strictEqual(list.length, 2);
});
test('should delete DAG', async () => {
await storage.save('to-delete', new Uint8Array([1]));
assert.ok(await storage.get('to-delete'));
await storage.delete('to-delete');
assert.strictEqual(await storage.get('to-delete'), null);
});
test('should find by name', async () => {
await storage.save('dag-1', new Uint8Array([1]), { name: 'query' });
await storage.save('dag-2', new Uint8Array([2]), { name: 'query' });
await storage.save('dag-3', new Uint8Array([3]), { name: 'other' });
const results = await storage.findByName('query');
assert.strictEqual(results.length, 2);
});
test('should calculate stats', async () => {
await storage.save('dag-1', new Uint8Array(100));
await storage.save('dag-2', new Uint8Array(200));
const stats = await storage.stats();
assert.strictEqual(stats.count, 2);
assert.strictEqual(stats.totalSize, 300);
});
test('should clear all', async () => {
await storage.save('dag-1', new Uint8Array([1]));
await storage.save('dag-2', new Uint8Array([2]));
await storage.clear();
const list = await storage.list();
assert.strictEqual(list.length, 0);
});
});
describe('createStorage', () => {
test('should create MemoryStorage in Node.js', () => {
const storage = createStorage();
assert.ok(storage instanceof MemoryStorage);
});
});

View File

@@ -0,0 +1,60 @@
/**
* @ruvector/rudag - Self-learning DAG query optimization
*
* Provides WASM-accelerated DAG operations with IndexedDB persistence
* for browser environments.
*/
export {
RuDag,
DagOperator,
AttentionMechanism,
type DagNode,
type DagEdge,
type CriticalPath,
type RuDagOptions,
} from './dag';
export {
DagStorage,
MemoryStorage,
createStorage,
isIndexedDBAvailable,
type StoredDag,
type DagStorageOptions,
} from './storage';
// Version info
export const VERSION = '0.1.0';
/**
* Quick start example:
*
* ```typescript
* import { RuDag, DagOperator, AttentionMechanism } from '@ruvector/rudag';
*
* // Create and initialize a DAG
* const dag = await new RuDag({ name: 'my-query' }).init();
*
* // Add nodes (query operators)
* const scan = dag.addNode(DagOperator.SCAN, 10.0);
* const filter = dag.addNode(DagOperator.FILTER, 2.0);
* const project = dag.addNode(DagOperator.PROJECT, 1.0);
*
* // Connect nodes
* dag.addEdge(scan, filter);
* dag.addEdge(filter, project);
*
* // Get critical path
* const { path, cost } = dag.criticalPath();
* console.log(`Critical path: ${path.join(' -> ')}, total cost: ${cost}`);
*
* // Compute attention scores
* const scores = dag.attention(AttentionMechanism.CRITICAL_PATH);
* console.log('Attention scores:', scores);
*
* // DAG is auto-saved to IndexedDB
* // Load it later
* const loadedDag = await RuDag.load(dag.getId());
* ```
*/

View File

@@ -0,0 +1,65 @@
/**
* Node.js-specific entry point with filesystem support
*
* @security Path traversal prevention via ID validation
*/
export * from './index';
import { RuDag } from './index';
/**
* Create a Node.js DAG with memory storage
*/
export declare function createNodeDag(name?: string): Promise<RuDag>;
/**
* Stored DAG metadata
*/
interface StoredMeta {
id: string;
name?: string;
metadata?: Record<string, unknown>;
createdAt: number;
updatedAt: number;
}
/**
* File-based storage for Node.js environments
* @security All file operations validate paths to prevent traversal attacks
*/
export declare class FileDagStorage {
private basePath;
private initialized;
constructor(basePath?: string);
init(): Promise<void>;
private getFilePath;
private getMetaPath;
save(id: string, data: Uint8Array, options?: {
name?: string;
metadata?: Record<string, unknown>;
}): Promise<void>;
load(id: string): Promise<Uint8Array | null>;
loadMeta(id: string): Promise<StoredMeta | null>;
delete(id: string): Promise<boolean>;
list(): Promise<string[]>;
clear(): Promise<void>;
stats(): Promise<{
count: number;
totalSize: number;
}>;
}
/**
* Node.js DAG manager with file persistence
*/
export declare class NodeDagManager {
private storage;
constructor(basePath?: string);
init(): Promise<void>;
createDag(name?: string): Promise<RuDag>;
saveDag(dag: RuDag): Promise<void>;
loadDag(id: string): Promise<RuDag | null>;
deleteDag(id: string): Promise<boolean>;
listDags(): Promise<string[]>;
clearAll(): Promise<void>;
getStats(): Promise<{
count: number;
totalSize: number;
}>;
}
//# sourceMappingURL=node.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,SAAS,CAAC;AAExB,OAAO,EAAE,KAAK,EAAiB,MAAM,SAAS,CAAC;AA6B/C;;GAEG;AACH,wBAAsB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAKjE;AAED;;GAEG;AACH,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAS;gBAEhB,QAAQ,GAAE,MAAiB;IAKjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAWb,WAAW;YAQX,WAAW;IAQnB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BtH,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAgB5C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAgBhD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAepC,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAiBzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,KAAK,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAkB7D;AAED;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAiB;gBAEpB,QAAQ,CAAC,EAAE,MAAM;IAIvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAMxC,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAQ1C,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIvC,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAI7B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAIzB,QAAQ,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAGhE"}

View File

@@ -0,0 +1,239 @@
"use strict";
/**
* Node.js-specific entry point with filesystem support
*
* @security Path traversal prevention via ID validation
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeDagManager = exports.FileDagStorage = void 0;
exports.createNodeDag = createNodeDag;
__exportStar(require("./index"), exports);
const index_1 = require("./index");
const fs_1 = require("fs");
const path_1 = require("path");
/**
* Validate storage ID to prevent path traversal attacks
* @security Only allows alphanumeric, dash, underscore characters
*/
function isValidStorageId(id) {
if (typeof id !== 'string' || id.length === 0 || id.length > 256)
return false;
// Strictly alphanumeric with dash/underscore - no dots, slashes, etc.
return /^[a-zA-Z0-9_-]+$/.test(id);
}
/**
* Ensure path is within base directory
* @security Prevents path traversal via realpath comparison
*/
async function ensureWithinBase(basePath, targetPath) {
const resolvedBase = (0, path_1.resolve)(basePath);
const resolvedTarget = (0, path_1.resolve)(targetPath);
if (!resolvedTarget.startsWith(resolvedBase + '/') && resolvedTarget !== resolvedBase) {
throw new Error('Path traversal detected: target path outside base directory');
}
return resolvedTarget;
}
/**
* Create a Node.js DAG with memory storage
*/
async function createNodeDag(name) {
const storage = new index_1.MemoryStorage();
const dag = new index_1.RuDag({ name, storage });
await dag.init();
return dag;
}
/**
* File-based storage for Node.js environments
* @security All file operations validate paths to prevent traversal attacks
*/
class FileDagStorage {
constructor(basePath = '.rudag') {
this.initialized = false;
// Normalize and resolve base path
this.basePath = (0, path_1.resolve)((0, path_1.normalize)(basePath));
}
async init() {
if (this.initialized)
return;
try {
await fs_1.promises.mkdir(this.basePath, { recursive: true });
this.initialized = true;
}
catch (error) {
throw new Error(`Failed to create storage directory: ${error}`);
}
}
async getFilePath(id) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const targetPath = (0, path_1.join)(this.basePath, `${id}.dag`);
return ensureWithinBase(this.basePath, targetPath);
}
async getMetaPath(id) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const targetPath = (0, path_1.join)(this.basePath, `${id}.meta.json`);
return ensureWithinBase(this.basePath, targetPath);
}
async save(id, data, options = {}) {
await this.init();
const filePath = await this.getFilePath(id);
const metaPath = await this.getMetaPath(id);
// Load existing metadata for createdAt preservation
let existingMeta = null;
try {
const metaContent = await fs_1.promises.readFile(metaPath, 'utf-8');
existingMeta = JSON.parse(metaContent);
}
catch {
// File doesn't exist or invalid - will create new
}
const now = Date.now();
const meta = {
id,
name: options.name,
metadata: options.metadata,
createdAt: existingMeta?.createdAt || now,
updatedAt: now,
};
// Write both files atomically (as much as possible)
await Promise.all([
fs_1.promises.writeFile(filePath, Buffer.from(data)),
fs_1.promises.writeFile(metaPath, JSON.stringify(meta, null, 2)),
]);
}
async load(id) {
await this.init();
const filePath = await this.getFilePath(id);
try {
const data = await fs_1.promises.readFile(filePath);
return new Uint8Array(data);
}
catch (error) {
if (error.code === 'ENOENT') {
return null;
}
throw error;
}
}
async loadMeta(id) {
await this.init();
const metaPath = await this.getMetaPath(id);
try {
const content = await fs_1.promises.readFile(metaPath, 'utf-8');
return JSON.parse(content);
}
catch (error) {
if (error.code === 'ENOENT') {
return null;
}
throw error;
}
}
async delete(id) {
await this.init();
const filePath = await this.getFilePath(id);
const metaPath = await this.getMetaPath(id);
const results = await Promise.allSettled([
fs_1.promises.unlink(filePath),
fs_1.promises.unlink(metaPath),
]);
// Return true if at least one file was deleted
return results.some(r => r.status === 'fulfilled');
}
async list() {
await this.init();
try {
const files = await fs_1.promises.readdir(this.basePath);
return files
.filter(f => f.endsWith('.dag'))
.map(f => f.slice(0, -4)) // Remove .dag extension
.filter(id => isValidStorageId(id)); // Extra safety filter
}
catch (error) {
if (error.code === 'ENOENT') {
return [];
}
throw error;
}
}
async clear() {
await this.init();
const ids = await this.list();
await Promise.all(ids.map(id => this.delete(id)));
}
async stats() {
await this.init();
const ids = await this.list();
let totalSize = 0;
for (const id of ids) {
try {
const filePath = await this.getFilePath(id);
const stat = await fs_1.promises.stat(filePath);
totalSize += stat.size;
}
catch {
// Skip files that can't be accessed
}
}
return { count: ids.length, totalSize };
}
}
exports.FileDagStorage = FileDagStorage;
/**
* Node.js DAG manager with file persistence
*/
class NodeDagManager {
constructor(basePath) {
this.storage = new FileDagStorage(basePath);
}
async init() {
await this.storage.init();
}
async createDag(name) {
const dag = new index_1.RuDag({ name, storage: null, autoSave: false });
await dag.init();
return dag;
}
async saveDag(dag) {
const data = dag.toBytes();
await this.storage.save(dag.getId(), data, { name: dag.getName() });
}
async loadDag(id) {
const data = await this.storage.load(id);
if (!data)
return null;
const meta = await this.storage.loadMeta(id);
return index_1.RuDag.fromBytes(data, { id, name: meta?.name });
}
async deleteDag(id) {
return this.storage.delete(id);
}
async listDags() {
return this.storage.list();
}
async clearAll() {
return this.storage.clear();
}
async getStats() {
return this.storage.stats();
}
}
exports.NodeDagManager = NodeDagManager;
//# sourceMappingURL=node.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,269 @@
/**
* Node.js-specific entry point with filesystem support
*
* @security Path traversal prevention via ID validation
*/
export * from './index';
import { RuDag, MemoryStorage } from './index';
import { promises as fs } from 'fs';
import { join, normalize, resolve } from 'path';
/**
* Validate storage ID to prevent path traversal attacks
* @security Only allows alphanumeric, dash, underscore characters
*/
function isValidStorageId(id: string): boolean {
if (typeof id !== 'string' || id.length === 0 || id.length > 256) return false;
// Strictly alphanumeric with dash/underscore - no dots, slashes, etc.
return /^[a-zA-Z0-9_-]+$/.test(id);
}
/**
* Ensure path is within base directory
* @security Prevents path traversal via realpath comparison
*/
async function ensureWithinBase(basePath: string, targetPath: string): Promise<string> {
const resolvedBase = resolve(basePath);
const resolvedTarget = resolve(targetPath);
if (!resolvedTarget.startsWith(resolvedBase + '/') && resolvedTarget !== resolvedBase) {
throw new Error('Path traversal detected: target path outside base directory');
}
return resolvedTarget;
}
/**
* Create a Node.js DAG with memory storage
*/
export async function createNodeDag(name?: string): Promise<RuDag> {
const storage = new MemoryStorage();
const dag = new RuDag({ name, storage });
await dag.init();
return dag;
}
/**
* Stored DAG metadata
*/
interface StoredMeta {
id: string;
name?: string;
metadata?: Record<string, unknown>;
createdAt: number;
updatedAt: number;
}
/**
* File-based storage for Node.js environments
* @security All file operations validate paths to prevent traversal attacks
*/
export class FileDagStorage {
private basePath: string;
private initialized = false;
constructor(basePath: string = '.rudag') {
// Normalize and resolve base path
this.basePath = resolve(normalize(basePath));
}
async init(): Promise<void> {
if (this.initialized) return;
try {
await fs.mkdir(this.basePath, { recursive: true });
this.initialized = true;
} catch (error) {
throw new Error(`Failed to create storage directory: ${error}`);
}
}
private async getFilePath(id: string): Promise<string> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const targetPath = join(this.basePath, `${id}.dag`);
return ensureWithinBase(this.basePath, targetPath);
}
private async getMetaPath(id: string): Promise<string> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const targetPath = join(this.basePath, `${id}.meta.json`);
return ensureWithinBase(this.basePath, targetPath);
}
async save(id: string, data: Uint8Array, options: { name?: string; metadata?: Record<string, unknown> } = {}): Promise<void> {
await this.init();
const filePath = await this.getFilePath(id);
const metaPath = await this.getMetaPath(id);
// Load existing metadata for createdAt preservation
let existingMeta: StoredMeta | null = null;
try {
const metaContent = await fs.readFile(metaPath, 'utf-8');
existingMeta = JSON.parse(metaContent) as StoredMeta;
} catch {
// File doesn't exist or invalid - will create new
}
const now = Date.now();
const meta: StoredMeta = {
id,
name: options.name,
metadata: options.metadata,
createdAt: existingMeta?.createdAt || now,
updatedAt: now,
};
// Write both files atomically (as much as possible)
await Promise.all([
fs.writeFile(filePath, Buffer.from(data)),
fs.writeFile(metaPath, JSON.stringify(meta, null, 2)),
]);
}
async load(id: string): Promise<Uint8Array | null> {
await this.init();
const filePath = await this.getFilePath(id);
try {
const data = await fs.readFile(filePath);
return new Uint8Array(data);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw error;
}
}
async loadMeta(id: string): Promise<StoredMeta | null> {
await this.init();
const metaPath = await this.getMetaPath(id);
try {
const content = await fs.readFile(metaPath, 'utf-8');
return JSON.parse(content) as StoredMeta;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw error;
}
}
async delete(id: string): Promise<boolean> {
await this.init();
const filePath = await this.getFilePath(id);
const metaPath = await this.getMetaPath(id);
const results = await Promise.allSettled([
fs.unlink(filePath),
fs.unlink(metaPath),
]);
// Return true if at least one file was deleted
return results.some(r => r.status === 'fulfilled');
}
async list(): Promise<string[]> {
await this.init();
try {
const files = await fs.readdir(this.basePath);
return files
.filter(f => f.endsWith('.dag'))
.map(f => f.slice(0, -4)) // Remove .dag extension
.filter(id => isValidStorageId(id)); // Extra safety filter
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return [];
}
throw error;
}
}
async clear(): Promise<void> {
await this.init();
const ids = await this.list();
await Promise.all(ids.map(id => this.delete(id)));
}
async stats(): Promise<{ count: number; totalSize: number }> {
await this.init();
const ids = await this.list();
let totalSize = 0;
for (const id of ids) {
try {
const filePath = await this.getFilePath(id);
const stat = await fs.stat(filePath);
totalSize += stat.size;
} catch {
// Skip files that can't be accessed
}
}
return { count: ids.length, totalSize };
}
}
/**
* Node.js DAG manager with file persistence
*/
export class NodeDagManager {
private storage: FileDagStorage;
constructor(basePath?: string) {
this.storage = new FileDagStorage(basePath);
}
async init(): Promise<void> {
await this.storage.init();
}
async createDag(name?: string): Promise<RuDag> {
const dag = new RuDag({ name, storage: null, autoSave: false });
await dag.init();
return dag;
}
async saveDag(dag: RuDag): Promise<void> {
const data = dag.toBytes();
await this.storage.save(dag.getId(), data, { name: dag.getName() });
}
async loadDag(id: string): Promise<RuDag | null> {
const data = await this.storage.load(id);
if (!data) return null;
const meta = await this.storage.loadMeta(id);
return RuDag.fromBytes(data, { id, name: meta?.name });
}
async deleteDag(id: string): Promise<boolean> {
return this.storage.delete(id);
}
async listDags(): Promise<string[]> {
return this.storage.list();
}
async clearAll(): Promise<void> {
return this.storage.clear();
}
async getStats(): Promise<{ count: number; totalSize: number }> {
return this.storage.stats();
}
}

View File

@@ -0,0 +1,136 @@
/**
* IndexedDB-based persistence layer for DAG storage
* Provides browser-compatible persistent storage for DAGs
*
* @performance Single-transaction pattern for atomic operations
* @security ID validation to prevent injection
*/
export interface StoredDag {
/** Unique identifier */
id: string;
/** Human-readable name */
name?: string;
/** Serialized DAG data */
data: Uint8Array;
/** Creation timestamp */
createdAt: number;
/** Last update timestamp */
updatedAt: number;
/** Optional metadata */
metadata?: Record<string, unknown>;
}
export interface DagStorageOptions {
/** Custom database name */
dbName?: string;
/** Database version for migrations */
version?: number;
}
/**
* Check if IndexedDB is available (browser environment)
*/
export declare function isIndexedDBAvailable(): boolean;
/**
* IndexedDB storage class for DAG persistence
*
* @performance Uses single-transaction pattern for save operations
*/
export declare class DagStorage {
private dbName;
private version;
private db;
private initialized;
constructor(options?: DagStorageOptions);
/**
* Initialize the database connection
* @throws {Error} If IndexedDB is not available
* @throws {Error} If database is blocked by another tab
*/
init(): Promise<void>;
/**
* Ensure database is initialized
* @throws {Error} If database not initialized
*/
private ensureInit;
/**
* Save a DAG to storage (single-transaction pattern)
* @performance Uses single transaction for atomic read-modify-write
*/
save(id: string, data: Uint8Array, options?: {
name?: string;
metadata?: Record<string, unknown>;
}): Promise<StoredDag>;
/**
* Save multiple DAGs in a single transaction (batch operation)
* @performance Much faster than individual saves for bulk operations
*/
saveBatch(dags: Array<{
id: string;
data: Uint8Array;
name?: string;
metadata?: Record<string, unknown>;
}>): Promise<StoredDag[]>;
/**
* Get a DAG from storage
*/
get(id: string): Promise<StoredDag | null>;
/**
* Delete a DAG from storage
*/
delete(id: string): Promise<boolean>;
/**
* List all DAGs in storage
*/
list(): Promise<StoredDag[]>;
/**
* Search DAGs by name
*/
findByName(name: string): Promise<StoredDag[]>;
/**
* Clear all DAGs from storage
*/
clear(): Promise<void>;
/**
* Get storage statistics
*/
stats(): Promise<{
count: number;
totalSize: number;
}>;
/**
* Close the database connection
*/
close(): void;
}
/**
* In-memory storage fallback for Node.js or environments without IndexedDB
*/
export declare class MemoryStorage {
private store;
private initialized;
init(): Promise<void>;
save(id: string, data: Uint8Array, options?: {
name?: string;
metadata?: Record<string, unknown>;
}): Promise<StoredDag>;
saveBatch(dags: Array<{
id: string;
data: Uint8Array;
name?: string;
metadata?: Record<string, unknown>;
}>): Promise<StoredDag[]>;
get(id: string): Promise<StoredDag | null>;
delete(id: string): Promise<boolean>;
list(): Promise<StoredDag[]>;
findByName(name: string): Promise<StoredDag[]>;
clear(): Promise<void>;
stats(): Promise<{
count: number;
totalSize: number;
}>;
close(): void;
}
/**
* Create appropriate storage based on environment
*/
export declare function createStorage(options?: DagStorageOptions): DagStorage | MemoryStorage;
//# sourceMappingURL=storage.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["storage.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,338 @@
"use strict";
/**
* IndexedDB-based persistence layer for DAG storage
* Provides browser-compatible persistent storage for DAGs
*
* @performance Single-transaction pattern for atomic operations
* @security ID validation to prevent injection
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemoryStorage = exports.DagStorage = void 0;
exports.isIndexedDBAvailable = isIndexedDBAvailable;
exports.createStorage = createStorage;
const DB_NAME = 'rudag-storage';
const DB_VERSION = 1;
const STORE_NAME = 'dags';
/**
* Validate storage ID
* @security Prevents injection attacks via ID
*/
function isValidStorageId(id) {
if (typeof id !== 'string' || id.length === 0 || id.length > 256)
return false;
return /^[a-zA-Z0-9_-]+$/.test(id);
}
/**
* Check if IndexedDB is available (browser environment)
*/
function isIndexedDBAvailable() {
return typeof indexedDB !== 'undefined';
}
/**
* IndexedDB storage class for DAG persistence
*
* @performance Uses single-transaction pattern for save operations
*/
class DagStorage {
constructor(options = {}) {
this.db = null;
this.initialized = false;
this.dbName = options.dbName || DB_NAME;
this.version = options.version || DB_VERSION;
}
/**
* Initialize the database connection
* @throws {Error} If IndexedDB is not available
* @throws {Error} If database is blocked by another tab
*/
async init() {
if (this.initialized && this.db)
return;
if (!isIndexedDBAvailable()) {
throw new Error('IndexedDB is not available in this environment');
}
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => {
reject(new Error(`Failed to open database: ${request.error?.message || 'Unknown error'}`));
};
request.onblocked = () => {
reject(new Error('Database blocked - please close other tabs using this application'));
};
request.onsuccess = () => {
this.db = request.result;
this.initialized = true;
// Handle connection errors after open
this.db.onerror = (event) => {
console.error('[DagStorage] Database error:', event);
};
// Handle version change (another tab upgraded)
this.db.onversionchange = () => {
this.db?.close();
this.db = null;
this.initialized = false;
console.warn('[DagStorage] Database version changed - connection closed');
};
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
store.createIndex('updatedAt', 'updatedAt', { unique: false });
}
};
});
}
/**
* Ensure database is initialized
* @throws {Error} If database not initialized
*/
ensureInit() {
if (!this.db) {
throw new Error('Database not initialized. Call init() first.');
}
return this.db;
}
/**
* Save a DAG to storage (single-transaction pattern)
* @performance Uses single transaction for atomic read-modify-write
*/
async save(id, data, options = {}) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const db = this.ensureInit();
const now = Date.now();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
// First, get existing record in same transaction
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const existing = getRequest.result;
const record = {
id,
name: options.name,
data,
createdAt: existing?.createdAt || now,
updatedAt: now,
metadata: options.metadata,
};
// Put in same transaction
const putRequest = store.put(record);
putRequest.onsuccess = () => resolve(record);
putRequest.onerror = () => reject(new Error(`Failed to save DAG: ${putRequest.error?.message}`));
};
getRequest.onerror = () => {
reject(new Error(`Failed to check existing DAG: ${getRequest.error?.message}`));
};
transaction.onerror = () => {
reject(new Error(`Transaction failed: ${transaction.error?.message}`));
};
});
}
/**
* Save multiple DAGs in a single transaction (batch operation)
* @performance Much faster than individual saves for bulk operations
*/
async saveBatch(dags) {
for (const dag of dags) {
if (!isValidStorageId(dag.id)) {
throw new Error(`Invalid storage ID: "${dag.id}". Must be alphanumeric with dashes/underscores only.`);
}
}
const db = this.ensureInit();
const now = Date.now();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const results = [];
let completed = 0;
for (const dag of dags) {
const getRequest = store.get(dag.id);
getRequest.onsuccess = () => {
const existing = getRequest.result;
const record = {
id: dag.id,
name: dag.name,
data: dag.data,
createdAt: existing?.createdAt || now,
updatedAt: now,
metadata: dag.metadata,
};
store.put(record);
results.push(record);
completed++;
};
}
transaction.oncomplete = () => resolve(results);
transaction.onerror = () => reject(new Error(`Batch save failed: ${transaction.error?.message}`));
});
}
/**
* Get a DAG from storage
*/
async get(id) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(new Error(`Failed to get DAG: ${request.error?.message}`));
});
}
/**
* Delete a DAG from storage
*/
async delete(id) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(id);
request.onsuccess = () => resolve(true);
request.onerror = () => reject(new Error(`Failed to delete DAG: ${request.error?.message}`));
});
}
/**
* List all DAGs in storage
*/
async list() {
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(new Error(`Failed to list DAGs: ${request.error?.message}`));
});
}
/**
* Search DAGs by name
*/
async findByName(name) {
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const index = store.index('name');
const request = index.getAll(name);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(new Error(`Failed to find DAGs by name: ${request.error?.message}`));
});
}
/**
* Clear all DAGs from storage
*/
async clear() {
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error(`Failed to clear storage: ${request.error?.message}`));
});
}
/**
* Get storage statistics
*/
async stats() {
const dags = await this.list();
const totalSize = dags.reduce((sum, dag) => sum + dag.data.byteLength, 0);
return { count: dags.length, totalSize };
}
/**
* Close the database connection
*/
close() {
if (this.db) {
this.db.close();
this.db = null;
}
this.initialized = false;
}
}
exports.DagStorage = DagStorage;
/**
* In-memory storage fallback for Node.js or environments without IndexedDB
*/
class MemoryStorage {
constructor() {
this.store = new Map();
this.initialized = false;
}
async init() {
this.initialized = true;
}
async save(id, data, options = {}) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const now = Date.now();
const existing = this.store.get(id);
const record = {
id,
name: options.name,
data,
createdAt: existing?.createdAt || now,
updatedAt: now,
metadata: options.metadata,
};
this.store.set(id, record);
return record;
}
async saveBatch(dags) {
return Promise.all(dags.map(dag => this.save(dag.id, dag.data, { name: dag.name, metadata: dag.metadata })));
}
async get(id) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
return this.store.get(id) || null;
}
async delete(id) {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
return this.store.delete(id);
}
async list() {
return Array.from(this.store.values());
}
async findByName(name) {
return Array.from(this.store.values()).filter(dag => dag.name === name);
}
async clear() {
this.store.clear();
}
async stats() {
const dags = Array.from(this.store.values());
const totalSize = dags.reduce((sum, dag) => sum + dag.data.byteLength, 0);
return { count: dags.length, totalSize };
}
close() {
this.initialized = false;
}
}
exports.MemoryStorage = MemoryStorage;
/**
* Create appropriate storage based on environment
*/
function createStorage(options = {}) {
if (isIndexedDBAvailable()) {
return new DagStorage(options);
}
return new MemoryStorage();
}
//# sourceMappingURL=storage.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,418 @@
/**
* IndexedDB-based persistence layer for DAG storage
* Provides browser-compatible persistent storage for DAGs
*
* @performance Single-transaction pattern for atomic operations
* @security ID validation to prevent injection
*/
const DB_NAME = 'rudag-storage';
const DB_VERSION = 1;
const STORE_NAME = 'dags';
export interface StoredDag {
/** Unique identifier */
id: string;
/** Human-readable name */
name?: string;
/** Serialized DAG data */
data: Uint8Array;
/** Creation timestamp */
createdAt: number;
/** Last update timestamp */
updatedAt: number;
/** Optional metadata */
metadata?: Record<string, unknown>;
}
export interface DagStorageOptions {
/** Custom database name */
dbName?: string;
/** Database version for migrations */
version?: number;
}
/**
* Validate storage ID
* @security Prevents injection attacks via ID
*/
function isValidStorageId(id: string): boolean {
if (typeof id !== 'string' || id.length === 0 || id.length > 256) return false;
return /^[a-zA-Z0-9_-]+$/.test(id);
}
/**
* Check if IndexedDB is available (browser environment)
*/
export function isIndexedDBAvailable(): boolean {
return typeof indexedDB !== 'undefined';
}
/**
* IndexedDB storage class for DAG persistence
*
* @performance Uses single-transaction pattern for save operations
*/
export class DagStorage {
private dbName: string;
private version: number;
private db: IDBDatabase | null = null;
private initialized = false;
constructor(options: DagStorageOptions = {}) {
this.dbName = options.dbName || DB_NAME;
this.version = options.version || DB_VERSION;
}
/**
* Initialize the database connection
* @throws {Error} If IndexedDB is not available
* @throws {Error} If database is blocked by another tab
*/
async init(): Promise<void> {
if (this.initialized && this.db) return;
if (!isIndexedDBAvailable()) {
throw new Error('IndexedDB is not available in this environment');
}
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => {
reject(new Error(`Failed to open database: ${request.error?.message || 'Unknown error'}`));
};
request.onblocked = () => {
reject(new Error('Database blocked - please close other tabs using this application'));
};
request.onsuccess = () => {
this.db = request.result;
this.initialized = true;
// Handle connection errors after open
this.db.onerror = (event) => {
console.error('[DagStorage] Database error:', event);
};
// Handle version change (another tab upgraded)
this.db.onversionchange = () => {
this.db?.close();
this.db = null;
this.initialized = false;
console.warn('[DagStorage] Database version changed - connection closed');
};
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
store.createIndex('updatedAt', 'updatedAt', { unique: false });
}
};
});
}
/**
* Ensure database is initialized
* @throws {Error} If database not initialized
*/
private ensureInit(): IDBDatabase {
if (!this.db) {
throw new Error('Database not initialized. Call init() first.');
}
return this.db;
}
/**
* Save a DAG to storage (single-transaction pattern)
* @performance Uses single transaction for atomic read-modify-write
*/
async save(id: string, data: Uint8Array, options: { name?: string; metadata?: Record<string, unknown> } = {}): Promise<StoredDag> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const db = this.ensureInit();
const now = Date.now();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
// First, get existing record in same transaction
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const existing = getRequest.result as StoredDag | undefined;
const record: StoredDag = {
id,
name: options.name,
data,
createdAt: existing?.createdAt || now,
updatedAt: now,
metadata: options.metadata,
};
// Put in same transaction
const putRequest = store.put(record);
putRequest.onsuccess = () => resolve(record);
putRequest.onerror = () => reject(new Error(`Failed to save DAG: ${putRequest.error?.message}`));
};
getRequest.onerror = () => {
reject(new Error(`Failed to check existing DAG: ${getRequest.error?.message}`));
};
transaction.onerror = () => {
reject(new Error(`Transaction failed: ${transaction.error?.message}`));
};
});
}
/**
* Save multiple DAGs in a single transaction (batch operation)
* @performance Much faster than individual saves for bulk operations
*/
async saveBatch(dags: Array<{ id: string; data: Uint8Array; name?: string; metadata?: Record<string, unknown> }>): Promise<StoredDag[]> {
for (const dag of dags) {
if (!isValidStorageId(dag.id)) {
throw new Error(`Invalid storage ID: "${dag.id}". Must be alphanumeric with dashes/underscores only.`);
}
}
const db = this.ensureInit();
const now = Date.now();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const results: StoredDag[] = [];
let completed = 0;
for (const dag of dags) {
const getRequest = store.get(dag.id);
getRequest.onsuccess = () => {
const existing = getRequest.result as StoredDag | undefined;
const record: StoredDag = {
id: dag.id,
name: dag.name,
data: dag.data,
createdAt: existing?.createdAt || now,
updatedAt: now,
metadata: dag.metadata,
};
store.put(record);
results.push(record);
completed++;
};
}
transaction.oncomplete = () => resolve(results);
transaction.onerror = () => reject(new Error(`Batch save failed: ${transaction.error?.message}`));
});
}
/**
* Get a DAG from storage
*/
async get(id: string): Promise<StoredDag | null> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(new Error(`Failed to get DAG: ${request.error?.message}`));
});
}
/**
* Delete a DAG from storage
*/
async delete(id: string): Promise<boolean> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(id);
request.onsuccess = () => resolve(true);
request.onerror = () => reject(new Error(`Failed to delete DAG: ${request.error?.message}`));
});
}
/**
* List all DAGs in storage
*/
async list(): Promise<StoredDag[]> {
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(new Error(`Failed to list DAGs: ${request.error?.message}`));
});
}
/**
* Search DAGs by name
*/
async findByName(name: string): Promise<StoredDag[]> {
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const index = store.index('name');
const request = index.getAll(name);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(new Error(`Failed to find DAGs by name: ${request.error?.message}`));
});
}
/**
* Clear all DAGs from storage
*/
async clear(): Promise<void> {
const db = this.ensureInit();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error(`Failed to clear storage: ${request.error?.message}`));
});
}
/**
* Get storage statistics
*/
async stats(): Promise<{ count: number; totalSize: number }> {
const dags = await this.list();
const totalSize = dags.reduce((sum, dag) => sum + dag.data.byteLength, 0);
return { count: dags.length, totalSize };
}
/**
* Close the database connection
*/
close(): void {
if (this.db) {
this.db.close();
this.db = null;
}
this.initialized = false;
}
}
/**
* In-memory storage fallback for Node.js or environments without IndexedDB
*/
export class MemoryStorage {
private store: Map<string, StoredDag> = new Map();
private initialized = false;
async init(): Promise<void> {
this.initialized = true;
}
async save(id: string, data: Uint8Array, options: { name?: string; metadata?: Record<string, unknown> } = {}): Promise<StoredDag> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
const now = Date.now();
const existing = this.store.get(id);
const record: StoredDag = {
id,
name: options.name,
data,
createdAt: existing?.createdAt || now,
updatedAt: now,
metadata: options.metadata,
};
this.store.set(id, record);
return record;
}
async saveBatch(dags: Array<{ id: string; data: Uint8Array; name?: string; metadata?: Record<string, unknown> }>): Promise<StoredDag[]> {
return Promise.all(dags.map(dag => this.save(dag.id, dag.data, { name: dag.name, metadata: dag.metadata })));
}
async get(id: string): Promise<StoredDag | null> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
return this.store.get(id) || null;
}
async delete(id: string): Promise<boolean> {
if (!isValidStorageId(id)) {
throw new Error(`Invalid storage ID: "${id}". Must be alphanumeric with dashes/underscores only.`);
}
return this.store.delete(id);
}
async list(): Promise<StoredDag[]> {
return Array.from(this.store.values());
}
async findByName(name: string): Promise<StoredDag[]> {
return Array.from(this.store.values()).filter(dag => dag.name === name);
}
async clear(): Promise<void> {
this.store.clear();
}
async stats(): Promise<{ count: number; totalSize: number }> {
const dags = Array.from(this.store.values());
const totalSize = dags.reduce((sum, dag) => sum + dag.data.byteLength, 0);
return { count: dags.length, totalSize };
}
close(): void {
this.initialized = false;
}
}
/**
* Create appropriate storage based on environment
*/
export function createStorage(options: DagStorageOptions = {}): DagStorage | MemoryStorage {
if (isIndexedDBAvailable()) {
return new DagStorage(options);
}
return new MemoryStorage();
}