616 lines
19 KiB
JavaScript
616 lines
19 KiB
JavaScript
"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
|