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,26 @@
# Dependencies
node_modules/
# Build artifacts (generated by npm run build)
# Note: dist/, pkg/, pkg-node/ are kept for publishing
# TypeScript cache
*.tsbuildinfo
# npm
npm-debug.log*
.npm
# Test coverage
coverage/
.nyc_output/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 rUv
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,757 @@
# @ruvector/rudag
[![npm version](https://img.shields.io/npm/v/@ruvector/rudag.svg)](https://www.npmjs.com/package/@ruvector/rudag)
[![npm downloads](https://img.shields.io/npm/dm/@ruvector/rudag.svg)](https://www.npmjs.com/package/@ruvector/rudag)
[![license](https://img.shields.io/npm/l/@ruvector/rudag.svg)](https://github.com/ruvnet/ruvector/blob/main/LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
[![WebAssembly](https://img.shields.io/badge/WebAssembly-Rust-orange.svg)](https://webassembly.org/)
[![Node.js](https://img.shields.io/badge/Node.js-16+-green.svg)](https://nodejs.org/)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@ruvector/rudag)](https://bundlephobia.com/package/@ruvector/rudag)
**Smart task scheduling with self-learning optimization — powered by Rust/WASM**
> *"What order should I run these tasks? Which one is slowing everything down?"*
rudag answers these questions instantly. It's a **Directed Acyclic Graph (DAG)** library that helps you manage dependencies, find bottlenecks, and optimize execution — all with self-learning intelligence that gets smarter over time.
## Installation
```bash
npm install @ruvector/rudag
```
```typescript
// 3 lines to find your bottleneck
const dag = new RuDag({ name: 'my-pipeline' });
await dag.init();
const { path, cost } = dag.criticalPath(); // → "Task A → Task C takes 8 seconds"
```
---
## The Problem
You have tasks with dependencies. **Task C** needs **A** and **B** to finish first:
```
┌─────────────┐ ┌─────────────┐
│ Task A: 5s │ │ Task B: 3s │
└──────┬──────┘ └──────┬──────┘
│ │
└────────┬──────────┘
┌─────────────┐
│ Task C: 2s │
└──────┬──────┘
┌─────────────┐
│ Task D: 1s │
└─────────────┘
```
**You need answers:**
| Question | rudag Method | Answer |
|----------|--------------|--------|
| What order to run tasks? | `topoSort()` | `[A, B, C, D]` |
| How long will it all take? | `criticalPath()` | `A→C→D = 8s` (B runs parallel) |
| What should I optimize? | `attention()` | Task A scores highest — fix that first! |
## Where You'll Use This
| Use Case | Example |
|----------|---------|
| 🗄️ **Query Optimization** | Find which table scan is the bottleneck |
| 🔨 **Build Systems** | Compile dependencies in the right order |
| 📦 **Package Managers** | Resolve and install dependencies |
| 🔄 **CI/CD Pipelines** | Orchestrate test → build → deploy |
| 📊 **ETL Pipelines** | Schedule extract → transform → load |
| 🎮 **Game AI** | Plan action sequences with prerequisites |
| 📋 **Workflow Engines** | Manage approval chains and state machines |
## Why rudag?
| Without rudag | With rudag |
|---------------|------------|
| Write graph algorithms from scratch | One-liner: `dag.criticalPath()` |
| Slow JavaScript loops | **Rust/WASM** - 10-100x faster |
| Data lost on page refresh | **Auto-saves** to IndexedDB |
| Hard to find bottlenecks | **Attention scores** highlight important nodes |
| Complex setup | `npm install` and go |
## Comparison with Alternatives
| Feature | rudag | graphlib | dagre | d3-dag |
|---------|-------|----------|-------|--------|
| **Performance** | ⚡ WASM (10-100x faster) | JS | JS | JS |
| **Critical Path** | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ Manual |
| **Attention/Scoring** | ✅ ML-inspired | ❌ | ❌ | ❌ |
| **Cycle Detection** | ✅ Automatic | ✅ | ✅ | ✅ |
| **Topological Sort** | ✅ | ✅ | ✅ | ✅ |
| **Persistence** | ✅ IndexedDB + Files | ❌ | ❌ | ❌ |
| **Browser + Node.js** | ✅ Both | ✅ Both | ✅ Both | ⚠️ Browser |
| **TypeScript** | ✅ Native | ⚠️ @types | ⚠️ @types | ✅ Native |
| **Bundle Size** | ~50KB (WASM) | ~15KB | ~30KB | ~20KB |
| **Self-Learning** | ✅ | ❌ | ❌ | ❌ |
| **Serialization** | ✅ JSON + Binary | ✅ JSON | ✅ JSON | ❌ |
| **CLI Tool** | ✅ | ❌ | ❌ | ❌ |
### When to Use What
| Use Case | Recommendation |
|----------|----------------|
| **Query optimization / Task scheduling** | **rudag** - Critical path + attention scoring |
| **Graph visualization / Layout** | **dagre** - Designed for layout algorithms |
| **Simple dependency tracking** | **graphlib** - Lightweight, no WASM overhead |
| **D3 integration** | **d3-dag** - Native D3 compatibility |
| **Large graphs (10k+ nodes)** | **rudag** - WASM performance advantage |
| **Offline-first apps** | **rudag** - Built-in persistence |
## Key Capabilities
### 🧠 Self-Learning Optimization
rudag uses **ML-inspired attention mechanisms** to learn which nodes matter most. The more you use it, the smarter it gets at identifying bottlenecks and suggesting optimizations.
```typescript
// Get importance scores for each node
const scores = dag.attention(AttentionMechanism.CRITICAL_PATH);
// Nodes on the critical path score higher → optimize these first!
```
### ⚡ WASM-Accelerated Performance
Core algorithms run in **Rust compiled to WebAssembly** - the same technology powering Figma, Google Earth, and AutoCAD in the browser. Get native-like speed without leaving JavaScript.
### 🔄 Automatic Cycle Detection
DAGs can't have cycles by definition. rudag **automatically prevents** invalid edges that would create loops:
```typescript
dag.addEdge(a, b); // ✅ OK
dag.addEdge(b, c); // ✅ OK
dag.addEdge(c, a); // ❌ Returns false - would create cycle!
```
### 📊 Critical Path Analysis
Instantly find the **longest path** through your graph - the sequence of tasks that determines total execution time. This is what you need to optimize first.
### 💾 Zero-Config Persistence
Your DAGs automatically save to **IndexedDB** in browsers or **files** in Node.js. No database setup, no configuration - just works.
### 🔌 Serialization & Interop
Export to **JSON** (human-readable) or **binary** (compact, fast). Share DAGs between services, store in databases, or send over the network.
## Quick Start
```typescript
import { RuDag, DagOperator } from '@ruvector/rudag';
// Create a DAG (auto-persists to IndexedDB in browser)
const dag = new RuDag({ name: 'my-query' });
await dag.init();
// Add nodes with operators and costs
const scan = dag.addNode(DagOperator.SCAN, 100); // Read table: 100ms
const filter = dag.addNode(DagOperator.FILTER, 10); // Filter rows: 10ms
const project = dag.addNode(DagOperator.PROJECT, 5); // Select columns: 5ms
// Connect nodes (creates edges)
dag.addEdge(scan, filter);
dag.addEdge(filter, project);
// Analyze the DAG
const topo = dag.topoSort(); // [0, 1, 2] - execution order
const { path, cost } = dag.criticalPath(); // Slowest path: 115ms
console.log(`Critical path: ${path.join(' → ')} (${cost}ms)`);
// Output: Critical path: 0 → 1 → 2 (115ms)
// Cleanup when done
dag.dispose();
```
## Features
### Core Operations
| Feature | Description |
|---------|-------------|
| `addNode(operator, cost)` | Add a node with operator type and execution cost |
| `addEdge(from, to)` | Connect nodes (rejects cycles automatically) |
| `topoSort()` | Get nodes in topological order |
| `criticalPath()` | Find the longest/most expensive path |
| `attention(mechanism)` | Score nodes by importance |
### Operators
```typescript
import { DagOperator } from '@ruvector/rudag';
DagOperator.SCAN // 0 - Table scan
DagOperator.FILTER // 1 - WHERE clause
DagOperator.PROJECT // 2 - SELECT columns
DagOperator.JOIN // 3 - Table join
DagOperator.AGGREGATE // 4 - GROUP BY
DagOperator.SORT // 5 - ORDER BY
DagOperator.LIMIT // 6 - LIMIT/TOP
DagOperator.UNION // 7 - UNION
DagOperator.CUSTOM // 255 - Custom operator
```
### Attention Mechanisms
Score nodes by their importance using ML-inspired attention:
```typescript
import { AttentionMechanism } from '@ruvector/rudag';
// Score by position in execution order
const topoScores = dag.attention(AttentionMechanism.TOPOLOGICAL);
// Score by distance from critical path (most useful)
const criticalScores = dag.attention(AttentionMechanism.CRITICAL_PATH);
// Equal scores for all nodes
const uniformScores = dag.attention(AttentionMechanism.UNIFORM);
```
### Persistence
**Browser (IndexedDB) - Automatic:**
```typescript
const dag = new RuDag({ name: 'my-dag' }); // Auto-saves to IndexedDB
await dag.init();
// Later: reload from storage
const loaded = await RuDag.load('dag-123456-abc');
// List all stored DAGs
const allDags = await RuDag.listStored();
// Delete a DAG
await RuDag.deleteStored('dag-123456-abc');
```
**Node.js (File System):**
```typescript
import { NodeDagManager } from '@ruvector/rudag/node';
const manager = new NodeDagManager('./.rudag');
await manager.init();
const dag = await manager.createDag('pipeline');
// ... build the DAG ...
await manager.saveDag(dag);
// Later: reload
const loaded = await manager.loadDag('pipeline-id');
```
**Disable Persistence:**
```typescript
const dag = new RuDag({ storage: null, autoSave: false });
```
### Serialization
```typescript
// Binary (compact, fast)
const bytes = dag.toBytes();
const restored = await RuDag.fromBytes(bytes);
// JSON (human-readable)
const json = dag.toJSON();
const restored = await RuDag.fromJSON(json);
```
## CLI Tool
After installing globally or in your project:
```bash
# If installed globally: npm install -g @ruvector/rudag
rudag create my-query > my-query.dag
# Or run directly with npx (no install needed)
npx @ruvector/rudag create my-query > my-query.dag
```
### Commands
```bash
# Create a sample DAG
rudag create my-query > my-query.dag
# Show DAG information
rudag info my-query.dag
# Print topological sort
rudag topo my-query.dag
# Find critical path
rudag critical my-query.dag
# Compute attention scores
rudag attention my-query.dag critical
# Convert between formats
rudag convert my-query.dag my-query.json
rudag convert my-query.json my-query.dag
# JSON output
rudag info my-query.dag --json
# Help
rudag help
```
## Use Cases
### 1. SQL Query Optimizer
Build a query plan DAG and find the critical path:
```typescript
import { RuDag, DagOperator } from '@ruvector/rudag';
async function analyzeQuery(sql: string) {
const dag = new RuDag({ name: sql.slice(0, 50) });
await dag.init();
// Parse SQL and build DAG (simplified example)
const scan1 = dag.addNode(DagOperator.SCAN, estimateScanCost('users'));
const scan2 = dag.addNode(DagOperator.SCAN, estimateScanCost('orders'));
const join = dag.addNode(DagOperator.JOIN, estimateJoinCost(1000, 5000));
const filter = dag.addNode(DagOperator.FILTER, 10);
const project = dag.addNode(DagOperator.PROJECT, 5);
dag.addEdge(scan1, join);
dag.addEdge(scan2, join);
dag.addEdge(join, filter);
dag.addEdge(filter, project);
const { path, cost } = dag.criticalPath();
console.log(`Estimated query time: ${cost}ms`);
console.log(`Bottleneck: node ${path[0]}`); // Usually the scan or join
return dag;
}
```
### 2. Task Scheduler
Schedule tasks respecting dependencies:
```typescript
import { RuDag, DagOperator } from '@ruvector/rudag';
interface Task {
id: string;
duration: number;
dependencies: string[];
}
async function scheduleTasks(tasks: Task[]) {
const dag = new RuDag({ name: 'task-schedule', storage: null });
await dag.init();
const taskToNode = new Map<string, number>();
// Add all tasks as nodes
for (const task of tasks) {
const nodeId = dag.addNode(DagOperator.CUSTOM, task.duration);
taskToNode.set(task.id, nodeId);
}
// Add dependencies as edges
for (const task of tasks) {
const toNode = taskToNode.get(task.id)!;
for (const dep of task.dependencies) {
const fromNode = taskToNode.get(dep)!;
dag.addEdge(fromNode, toNode);
}
}
// Get execution order
const order = dag.topoSort();
const schedule = order.map(nodeId => {
const task = tasks.find(t => taskToNode.get(t.id) === nodeId)!;
return task.id;
});
// Total time (critical path)
const { cost } = dag.criticalPath();
console.log(`Total time with parallelization: ${cost}ms`);
dag.dispose();
return schedule;
}
```
### 3. Build System
```typescript
import { RuDag, DagOperator } from '@ruvector/rudag';
const dag = new RuDag({ name: 'build' });
await dag.init();
// Define build steps
const compile = dag.addNode(DagOperator.CUSTOM, 5000); // 5s
const test = dag.addNode(DagOperator.CUSTOM, 10000); // 10s
const lint = dag.addNode(DagOperator.CUSTOM, 2000); // 2s
const bundle = dag.addNode(DagOperator.CUSTOM, 3000); // 3s
const deploy = dag.addNode(DagOperator.CUSTOM, 1000); // 1s
dag.addEdge(compile, test);
dag.addEdge(compile, lint);
dag.addEdge(test, bundle);
dag.addEdge(lint, bundle);
dag.addEdge(bundle, deploy);
// Parallel execution order
const order = dag.topoSort(); // [compile, test|lint (parallel), bundle, deploy]
// Critical path: compile → test → bundle → deploy = 19s
const { cost } = dag.criticalPath();
console.log(`Minimum build time: ${cost}ms`);
```
### 4. Data Pipeline (ETL)
```typescript
import { RuDag, DagOperator, AttentionMechanism } from '@ruvector/rudag';
const pipeline = new RuDag({ name: 'etl-pipeline' });
await pipeline.init();
// Extract
const extractUsers = pipeline.addNode(DagOperator.SCAN, 1000);
const extractOrders = pipeline.addNode(DagOperator.SCAN, 2000);
const extractProducts = pipeline.addNode(DagOperator.SCAN, 500);
// Transform
const cleanUsers = pipeline.addNode(DagOperator.FILTER, 100);
const joinData = pipeline.addNode(DagOperator.JOIN, 3000);
const aggregate = pipeline.addNode(DagOperator.AGGREGATE, 500);
// Load
const loadWarehouse = pipeline.addNode(DagOperator.CUSTOM, 1000);
// Wire it up
pipeline.addEdge(extractUsers, cleanUsers);
pipeline.addEdge(cleanUsers, joinData);
pipeline.addEdge(extractOrders, joinData);
pipeline.addEdge(extractProducts, joinData);
pipeline.addEdge(joinData, aggregate);
pipeline.addEdge(aggregate, loadWarehouse);
// Find bottlenecks using attention scores
const scores = pipeline.attention(AttentionMechanism.CRITICAL_PATH);
console.log('Node importance:', scores);
// Nodes on critical path have higher scores
```
## Integration with Other Packages
### With Express.js (REST API)
```typescript
import express from 'express';
import { RuDag, DagOperator } from '@ruvector/rudag';
import { NodeDagManager } from '@ruvector/rudag/node';
const app = express();
const manager = new NodeDagManager('./data/dags');
app.use(express.json());
app.post('/dags', async (req, res) => {
const dag = await manager.createDag(req.body.name);
// ... add nodes from request ...
await manager.saveDag(dag);
res.json({ id: dag.getId() });
});
app.get('/dags/:id/critical-path', async (req, res) => {
const dag = await manager.loadDag(req.params.id);
if (!dag) return res.status(404).json({ error: 'Not found' });
const result = dag.criticalPath();
dag.dispose();
res.json(result);
});
app.listen(3000);
```
### With React (State Management)
```typescript
import { useState, useEffect } from 'react';
import { RuDag, DagOperator } from '@ruvector/rudag';
function useDag(name: string) {
const [dag, setDag] = useState<RuDag | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const init = async () => {
const d = new RuDag({ name });
await d.init();
setDag(d);
setLoading(false);
};
init();
return () => dag?.dispose();
}, [name]);
return { dag, loading };
}
function DagViewer({ name }: { name: string }) {
const { dag, loading } = useDag(name);
const [criticalPath, setCriticalPath] = useState<number[]>([]);
useEffect(() => {
if (dag && dag.nodeCount > 0) {
setCriticalPath(dag.criticalPath().path);
}
}, [dag]);
if (loading) return <div>Loading...</div>;
return (
<div>
<p>Nodes: {dag?.nodeCount}</p>
<p>Critical Path: {criticalPath.join(' → ')}</p>
</div>
);
}
```
### With D3.js (Visualization)
```typescript
import * as d3 from 'd3';
import { RuDag, DagOperator } from '@ruvector/rudag';
async function visualizeDag(dag: RuDag, container: HTMLElement) {
const nodes = dag.getNodes().map(n => ({
id: n.id,
label: DagOperator[n.operator],
cost: n.cost,
}));
const topo = dag.topoSort();
const { path: criticalPath } = dag.criticalPath();
const criticalSet = new Set(criticalPath);
// Create D3 visualization
const svg = d3.select(container).append('svg');
svg.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', d => Math.sqrt(d.cost) * 2)
.attr('fill', d => criticalSet.has(d.id) ? '#ff6b6b' : '#4dabf7')
.attr('cx', (d, i) => 100 + topo.indexOf(d.id) * 150)
.attr('cy', 100);
}
```
### With Bull (Job Queue)
```typescript
import Queue from 'bull';
import { RuDag, DagOperator } from '@ruvector/rudag';
const jobQueue = new Queue('dag-jobs');
async function queueDagExecution(dag: RuDag) {
const order = dag.topoSort();
const nodes = dag.getNodes();
// Queue jobs in topological order with dependencies
const jobIds: Record<number, string> = {};
for (const nodeId of order) {
const node = nodes.find(n => n.id === nodeId)!;
const job = await jobQueue.add({
nodeId,
operator: node.operator,
cost: node.cost,
}, {
// Jobs wait for their dependencies
delay: 0,
});
jobIds[nodeId] = job.id as string;
}
return jobIds;
}
```
### With GraphQL
```typescript
import { ApolloServer, gql } from 'apollo-server';
import { RuDag, DagOperator } from '@ruvector/rudag';
import { NodeDagManager } from '@ruvector/rudag/node';
const manager = new NodeDagManager('./dags');
const typeDefs = gql`
type Dag {
id: String!
name: String
nodeCount: Int!
edgeCount: Int!
criticalPath: CriticalPath!
}
type CriticalPath {
path: [Int!]!
cost: Float!
}
type Query {
dag(id: String!): Dag
dags: [Dag!]!
}
`;
const resolvers = {
Query: {
dag: async (_: any, { id }: { id: string }) => {
const dag = await manager.loadDag(id);
if (!dag) return null;
const result = {
id: dag.getId(),
name: dag.getName(),
nodeCount: dag.nodeCount,
edgeCount: dag.edgeCount,
criticalPath: dag.criticalPath(),
};
dag.dispose();
return result;
},
},
};
```
### With RxJS (Reactive Streams)
```typescript
import { Subject, from } from 'rxjs';
import { mergeMap, toArray } from 'rxjs/operators';
import { RuDag, DagOperator } from '@ruvector/rudag';
async function executeWithRxJS(dag: RuDag) {
const order = dag.topoSort();
const nodes = dag.getNodes();
const results$ = from(order).pipe(
mergeMap(async (nodeId) => {
const node = nodes.find(n => n.id === nodeId)!;
// Simulate execution
await new Promise(r => setTimeout(r, node.cost));
return { nodeId, completed: true };
}, 3), // Max 3 concurrent executions
toArray()
);
return results$.toPromise();
}
```
## Performance
| Operation | rudag (WASM) | Pure JS |
|-----------|--------------|---------|
| Add 10k nodes | ~15ms | ~150ms |
| Topological sort (10k) | ~2ms | ~50ms |
| Critical path (10k) | ~3ms | ~80ms |
| Serialization (10k) | ~5ms | ~100ms |
## Browser Support
- Chrome 57+
- Firefox 52+
- Safari 11+
- Edge 79+
Requires WebAssembly support.
## API Reference
### RuDag
```typescript
class RuDag {
constructor(options?: RuDagOptions);
init(): Promise<this>;
// Graph operations
addNode(operator: DagOperator, cost: number, metadata?: object): number;
addEdge(from: number, to: number): boolean;
// Properties
nodeCount: number;
edgeCount: number;
// Analysis
topoSort(): number[];
criticalPath(): { path: number[]; cost: number };
attention(mechanism?: AttentionMechanism): number[];
// Node access
getNode(id: number): DagNode | undefined;
getNodes(): DagNode[];
// Serialization
toBytes(): Uint8Array;
toJSON(): string;
// Persistence
save(): Promise<StoredDag | null>;
static load(id: string, storage?): Promise<RuDag | null>;
static fromBytes(data: Uint8Array, options?): Promise<RuDag>;
static fromJSON(json: string, options?): Promise<RuDag>;
static listStored(storage?): Promise<StoredDag[]>;
static deleteStored(id: string, storage?): Promise<boolean>;
// Lifecycle
getId(): string;
getName(): string | undefined;
setName(name: string): void;
dispose(): void;
}
```
### Options
```typescript
interface RuDagOptions {
id?: string; // Custom ID (auto-generated if not provided)
name?: string; // Human-readable name
storage?: Storage | null; // Persistence backend (null = disabled)
autoSave?: boolean; // Auto-save on changes (default: true)
onSaveError?: (error) => void; // Handle background save errors
}
```
## License
MIT OR Apache-2.0

View File

@@ -0,0 +1,378 @@
#!/usr/bin/env node
/**
* rudag CLI - Command-line interface for DAG operations
*
* @security File paths are validated to prevent reading arbitrary files
*/
const path = require('path');
const fs = require('fs');
// Lazy load to improve startup time
let RuDag, DagOperator, AttentionMechanism;
const args = process.argv.slice(2);
const command = args[0];
const help = `
rudag - Self-learning DAG query optimization CLI
Usage: rudag <command> [options]
Commands:
create <name> Create a new DAG and output to stdout
load <file> Load DAG from file (must be .dag or .json)
info <file> Show DAG information
topo <file> Print topological sort
critical <file> Find critical path
attention <file> [type] Compute attention scores (type: topo|critical|uniform)
convert <in> <out> Convert between JSON and binary formats
help Show this help message
Examples:
rudag create my-query > my-query.dag
rudag info ./data/my-query.dag
rudag critical ./queries/query.dag
rudag attention query.dag critical
Options:
--json Output in JSON format
--verbose Verbose output
Security:
- Only .dag and .json files are allowed
- Paths are restricted to current directory and subdirectories
`;
/**
* Validate file path for security
* @security Prevents path traversal and restricts to allowed extensions
*/
function validateFilePath(filePath) {
if (!filePath || typeof filePath !== 'string') {
throw new Error('File path is required');
}
// Check extension
const ext = path.extname(filePath).toLowerCase();
if (ext !== '.dag' && ext !== '.json') {
throw new Error(`Invalid file extension: ${ext}. Only .dag and .json files are allowed.`);
}
// Resolve to absolute path
const resolved = path.resolve(filePath);
const cwd = process.cwd();
// Ensure path is within current directory (prevents traversal)
if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
// Allow absolute paths within cwd or relative paths
const normalized = path.normalize(filePath);
if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
// Check if absolute path is within cwd
if (!resolved.startsWith(cwd)) {
throw new Error('Access denied: file path must be within current directory');
}
}
}
// Additional check: no null bytes
if (filePath.includes('\0')) {
throw new Error('Invalid file path: contains null bytes');
}
return resolved;
}
/**
* Validate output file path
*/
function validateOutputPath(filePath) {
if (!filePath || typeof filePath !== 'string') {
throw new Error('Output file path is required');
}
const ext = path.extname(filePath).toLowerCase();
if (ext !== '.dag' && ext !== '.json') {
throw new Error(`Invalid output extension: ${ext}. Only .dag and .json files are allowed.`);
}
const resolved = path.resolve(filePath);
const cwd = process.cwd();
// Must be within current directory
if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
throw new Error('Access denied: output path must be within current directory');
}
if (filePath.includes('\0')) {
throw new Error('Invalid file path: contains null bytes');
}
return resolved;
}
/**
* Lazy load dependencies
*/
async function loadDependencies() {
if (!RuDag) {
const mod = require('../dist/index.js');
RuDag = mod.RuDag;
DagOperator = mod.DagOperator;
AttentionMechanism = mod.AttentionMechanism;
}
}
async function main() {
if (!command || command === 'help' || command === '--help') {
console.log(help);
process.exit(0);
}
const isJson = args.includes('--json');
const verbose = args.includes('--verbose');
try {
await loadDependencies();
switch (command) {
case 'create': {
const name = args[1] || 'untitled';
// Validate name (alphanumeric only)
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
throw new Error('Invalid name: must be alphanumeric with dashes/underscores only');
}
const dag = new RuDag({ name, storage: null, autoSave: false });
await dag.init();
// Create a simple example DAG
const scan = dag.addNode(DagOperator.SCAN, 10.0);
const filter = dag.addNode(DagOperator.FILTER, 2.0);
const project = dag.addNode(DagOperator.PROJECT, 1.0);
dag.addEdge(scan, filter);
dag.addEdge(filter, project);
if (isJson) {
console.log(dag.toJSON());
} else {
const bytes = dag.toBytes();
process.stdout.write(Buffer.from(bytes));
}
dag.dispose();
break;
}
case 'load': {
const file = validateFilePath(args[1]);
if (!fs.existsSync(file)) {
throw new Error(`File not found: ${args[1]}`);
}
const data = fs.readFileSync(file);
let dag;
if (file.endsWith('.json')) {
dag = await RuDag.fromJSON(data.toString(), { storage: null });
} else {
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
}
console.log(`Loaded DAG with ${dag.nodeCount} nodes and ${dag.edgeCount} edges`);
dag.dispose();
break;
}
case 'info': {
const file = validateFilePath(args[1]);
if (!fs.existsSync(file)) {
throw new Error(`File not found: ${args[1]}`);
}
const data = fs.readFileSync(file);
let dag;
if (file.endsWith('.json')) {
dag = await RuDag.fromJSON(data.toString(), { storage: null });
} else {
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
}
const critPath = dag.criticalPath();
const info = {
file: args[1],
nodes: dag.nodeCount,
edges: dag.edgeCount,
criticalPath: critPath,
};
if (isJson) {
console.log(JSON.stringify(info, null, 2));
} else {
console.log(`File: ${info.file}`);
console.log(`Nodes: ${info.nodes}`);
console.log(`Edges: ${info.edges}`);
console.log(`Critical Path: ${info.criticalPath.path.join(' -> ')}`);
console.log(`Total Cost: ${info.criticalPath.cost}`);
}
dag.dispose();
break;
}
case 'topo': {
const file = validateFilePath(args[1]);
if (!fs.existsSync(file)) {
throw new Error(`File not found: ${args[1]}`);
}
const data = fs.readFileSync(file);
let dag;
if (file.endsWith('.json')) {
dag = await RuDag.fromJSON(data.toString(), { storage: null });
} else {
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
}
const topo = dag.topoSort();
if (isJson) {
console.log(JSON.stringify(topo));
} else {
console.log('Topological order:', topo.join(' -> '));
}
dag.dispose();
break;
}
case 'critical': {
const file = validateFilePath(args[1]);
if (!fs.existsSync(file)) {
throw new Error(`File not found: ${args[1]}`);
}
const data = fs.readFileSync(file);
let dag;
if (file.endsWith('.json')) {
dag = await RuDag.fromJSON(data.toString(), { storage: null });
} else {
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
}
const result = dag.criticalPath();
if (isJson) {
console.log(JSON.stringify(result));
} else {
console.log('Critical Path:', result.path.join(' -> '));
console.log('Total Cost:', result.cost);
}
dag.dispose();
break;
}
case 'attention': {
const file = validateFilePath(args[1]);
const type = args[2] || 'critical';
if (!fs.existsSync(file)) {
throw new Error(`File not found: ${args[1]}`);
}
const data = fs.readFileSync(file);
let dag;
if (file.endsWith('.json')) {
dag = await RuDag.fromJSON(data.toString(), { storage: null });
} else {
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
}
let mechanism;
switch (type) {
case 'topo':
case 'topological':
mechanism = AttentionMechanism.TOPOLOGICAL;
break;
case 'critical':
case 'critical_path':
mechanism = AttentionMechanism.CRITICAL_PATH;
break;
case 'uniform':
mechanism = AttentionMechanism.UNIFORM;
break;
default:
dag.dispose();
throw new Error(`Unknown attention type: ${type}. Use: topo, critical, or uniform`);
}
const scores = dag.attention(mechanism);
if (isJson) {
console.log(JSON.stringify({ type, scores }));
} else {
console.log(`Attention type: ${type}`);
scores.forEach((score, i) => {
console.log(` Node ${i}: ${score.toFixed(4)}`);
});
}
dag.dispose();
break;
}
case 'convert': {
const inFile = validateFilePath(args[1]);
const outFile = validateOutputPath(args[2]);
if (!fs.existsSync(inFile)) {
throw new Error(`Input file not found: ${args[1]}`);
}
const data = fs.readFileSync(inFile);
let dag;
if (inFile.endsWith('.json')) {
dag = await RuDag.fromJSON(data.toString(), { storage: null });
} else {
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
}
if (outFile.endsWith('.json')) {
fs.writeFileSync(outFile, dag.toJSON());
} else {
fs.writeFileSync(outFile, Buffer.from(dag.toBytes()));
}
console.log(`Converted ${args[1]} -> ${args[2]}`);
dag.dispose();
break;
}
default:
console.error(`Unknown command: ${command}`);
console.log('Run "rudag help" for usage information');
process.exit(1);
}
} catch (error) {
console.error('Error:', error.message);
if (verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,110 @@
{
"name": "@ruvector/rudag",
"version": "0.1.0",
"description": "Fast DAG (Directed Acyclic Graph) library with Rust/WASM. Topological sort, critical path, task scheduling, dependency resolution, workflow optimization. Self-learning ML attention. Browser & Node.js with auto-persistence.",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"rudag": "./bin/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"import": "./dist/browser.js",
"require": "./dist/browser.js"
},
"./node": {
"types": "./dist/node.d.ts",
"import": "./dist/node.js",
"require": "./dist/node.js"
},
"./wasm": {
"types": "./pkg/ruvector_dag_wasm.d.ts",
"import": "./pkg/ruvector_dag_wasm.js",
"require": "./pkg-node/ruvector_dag_wasm.js"
}
},
"files": [
"dist",
"pkg",
"pkg-node",
"bin",
"README.md",
"LICENSE"
],
"scripts": {
"build:wasm": "npm run build:wasm:bundler && npm run build:wasm:node",
"build:wasm:bundler": "cd ../../../crates/ruvector-dag-wasm && wasm-pack build --target bundler --out-dir ../../npm/packages/rudag/pkg",
"build:wasm:node": "cd ../../../crates/ruvector-dag-wasm && wasm-pack build --target nodejs --out-dir ../../npm/packages/rudag/pkg-node",
"build:ts": "tsc && tsc -p tsconfig.esm.json",
"build": "npm run build:wasm && npm run build:ts",
"test": "node --test dist/**/*.test.js",
"prepublishOnly": "npm run build"
},
"keywords": [
"dag",
"directed-acyclic-graph",
"graph",
"topological-sort",
"topo-sort",
"critical-path",
"task-scheduler",
"task-scheduling",
"job-scheduler",
"dependency-graph",
"dependency-resolution",
"workflow",
"workflow-engine",
"pipeline",
"data-pipeline",
"etl",
"build-system",
"wasm",
"webassembly",
"rust",
"indexeddb",
"persistence",
"query-optimizer",
"sql-optimizer",
"self-learning",
"machine-learning",
"attention-mechanism",
"bottleneck",
"performance",
"typescript",
"browser",
"nodejs"
],
"author": "rUv Team <team@ruv.io>",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector.git",
"directory": "npm/packages/rudag"
},
"bugs": {
"url": "https://github.com/ruvnet/ruvector/issues"
},
"homepage": "https://github.com/ruvnet/ruvector/tree/main/crates/ruvector-dag",
"engines": {
"node": ">= 16"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"devDependencies": {
"@types/node": "^20.19.25",
"typescript": "^5.9.3"
},
"dependencies": {
"idb": "^8.0.0"
}
}

View File

@@ -0,0 +1 @@
# Keep WASM files for npm

View File

@@ -0,0 +1,16 @@
{
"name": "ruvector-dag-wasm",
"collaborators": [
"RuVector Contributors"
],
"description": "Minimal WASM DAG library for browser and embedded systems",
"version": "0.1.0",
"license": "MIT OR Apache-2.0",
"files": [
"ruvector_dag_wasm_bg.wasm",
"ruvector_dag_wasm.js",
"ruvector_dag_wasm.d.ts"
],
"main": "ruvector_dag_wasm.js",
"types": "ruvector_dag_wasm.d.ts"
}

View File

@@ -0,0 +1,60 @@
/* tslint:disable */
/* eslint-disable */
export class WasmDag {
free(): void;
[Symbol.dispose](): void;
/**
* Get number of edges
*/
edge_count(): number;
/**
* Deserialize from bytes
*/
static from_bytes(data: Uint8Array): WasmDag;
/**
* Get number of nodes
*/
node_count(): number;
/**
* Find critical path (longest path by cost)
* Returns JSON: {"path": [node_ids], "cost": total}
*/
critical_path(): any;
/**
* Create new empty DAG
*/
constructor();
/**
* Serialize to JSON
*/
to_json(): string;
/**
* Add edge from -> to
* Returns false if creates cycle (simple check)
*/
add_edge(from: number, to: number): boolean;
/**
* Add a node with operator type and cost
* Returns node ID
*/
add_node(op: number, cost: number): number;
/**
* Serialize to bytes (bincode format)
*/
to_bytes(): Uint8Array;
/**
* Compute attention scores for nodes
* mechanism: 0=topological, 1=critical_path, 2=uniform
*/
attention(mechanism: number): Float32Array;
/**
* Deserialize from JSON
*/
static from_json(json: string): WasmDag;
/**
* Topological sort using Kahn's algorithm
* Returns node IDs in topological order
*/
topo_sort(): Uint32Array;
}

View File

@@ -0,0 +1,367 @@
let imports = {};
imports['__wbindgen_placeholder__'] = module.exports;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function getArrayF32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
function getArrayU32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
function getArrayU8FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
let cachedFloat32ArrayMemory0 = null;
function getFloat32ArrayMemory0() {
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
}
return cachedFloat32ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return decodeText(ptr, len);
}
let cachedUint32ArrayMemory0 = null;
function getUint32ArrayMemory0() {
if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) {
cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer);
}
return cachedUint32ArrayMemory0;
}
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getObject(idx) { return heap[idx]; }
let heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1, 1) >>> 0;
getUint8ArrayMemory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = cachedTextEncoder.encodeInto(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
function decodeText(ptr, len) {
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
const cachedTextEncoder = new TextEncoder();
if (!('encodeInto' in cachedTextEncoder)) {
cachedTextEncoder.encodeInto = function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
}
}
let WASM_VECTOR_LEN = 0;
const WasmDagFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_wasmdag_free(ptr >>> 0, 1));
/**
* Minimal DAG structure for WASM
* Self-contained with no external dependencies beyond wasm-bindgen
*/
class WasmDag {
static __wrap(ptr) {
ptr = ptr >>> 0;
const obj = Object.create(WasmDag.prototype);
obj.__wbg_ptr = ptr;
WasmDagFinalization.register(obj, obj.__wbg_ptr, obj);
return obj;
}
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
WasmDagFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_wasmdag_free(ptr, 0);
}
/**
* Get number of edges
* @returns {number}
*/
edge_count() {
const ret = wasm.wasmdag_edge_count(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Deserialize from bytes
* @param {Uint8Array} data
* @returns {WasmDag}
*/
static from_bytes(data) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm.wasmdag_from_bytes(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return WasmDag.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Get number of nodes
* @returns {number}
*/
node_count() {
const ret = wasm.wasmdag_node_count(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Find critical path (longest path by cost)
* Returns JSON: {"path": [node_ids], "cost": total}
* @returns {any}
*/
critical_path() {
const ret = wasm.wasmdag_critical_path(this.__wbg_ptr);
return takeObject(ret);
}
/**
* Create new empty DAG
*/
constructor() {
const ret = wasm.wasmdag_new();
this.__wbg_ptr = ret >>> 0;
WasmDagFinalization.register(this, this.__wbg_ptr, this);
return this;
}
/**
* Serialize to JSON
* @returns {string}
*/
to_json() {
let deferred1_0;
let deferred1_1;
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_to_json(retptr, this.__wbg_ptr);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
deferred1_0 = r0;
deferred1_1 = r1;
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_export2(deferred1_0, deferred1_1, 1);
}
}
/**
* Add edge from -> to
* Returns false if creates cycle (simple check)
* @param {number} from
* @param {number} to
* @returns {boolean}
*/
add_edge(from, to) {
const ret = wasm.wasmdag_add_edge(this.__wbg_ptr, from, to);
return ret !== 0;
}
/**
* Add a node with operator type and cost
* Returns node ID
* @param {number} op
* @param {number} cost
* @returns {number}
*/
add_node(op, cost) {
const ret = wasm.wasmdag_add_node(this.__wbg_ptr, op, cost);
return ret >>> 0;
}
/**
* Serialize to bytes (bincode format)
* @returns {Uint8Array}
*/
to_bytes() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_to_bytes(retptr, this.__wbg_ptr);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_export2(r0, r1 * 1, 1);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Compute attention scores for nodes
* mechanism: 0=topological, 1=critical_path, 2=uniform
* @param {number} mechanism
* @returns {Float32Array}
*/
attention(mechanism) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_attention(retptr, this.__wbg_ptr, mechanism);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export2(r0, r1 * 4, 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Deserialize from JSON
* @param {string} json
* @returns {WasmDag}
*/
static from_json(json) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(json, wasm.__wbindgen_export, wasm.__wbindgen_export3);
const len0 = WASM_VECTOR_LEN;
wasm.wasmdag_from_json(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return WasmDag.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Topological sort using Kahn's algorithm
* Returns node IDs in topological order
* @returns {Uint32Array}
*/
topo_sort() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_topo_sort(retptr, this.__wbg_ptr);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v1 = getArrayU32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export2(r0, r1 * 4, 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
}
if (Symbol.dispose) WasmDag.prototype[Symbol.dispose] = WasmDag.prototype.free;
exports.WasmDag = WasmDag;
exports.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
exports.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) {
// Cast intrinsic for `Ref(String) -> Externref`.
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
const wasmPath = `${__dirname}/ruvector_dag_wasm_bg.wasm`;
const wasmBytes = require('fs').readFileSync(wasmPath);
const wasmModule = new WebAssembly.Module(wasmBytes);
const wasm = exports.__wasm = new WebAssembly.Instance(wasmModule, imports).exports;

View File

@@ -0,0 +1,20 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const __wbg_wasmdag_free: (a: number, b: number) => void;
export const wasmdag_add_edge: (a: number, b: number, c: number) => number;
export const wasmdag_add_node: (a: number, b: number, c: number) => number;
export const wasmdag_attention: (a: number, b: number, c: number) => void;
export const wasmdag_critical_path: (a: number) => number;
export const wasmdag_edge_count: (a: number) => number;
export const wasmdag_from_bytes: (a: number, b: number, c: number) => void;
export const wasmdag_from_json: (a: number, b: number, c: number) => void;
export const wasmdag_new: () => number;
export const wasmdag_node_count: (a: number) => number;
export const wasmdag_to_bytes: (a: number, b: number) => void;
export const wasmdag_to_json: (a: number, b: number) => void;
export const wasmdag_topo_sort: (a: number, b: number) => void;
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
export const __wbindgen_export: (a: number, b: number) => number;
export const __wbindgen_export2: (a: number, b: number, c: number) => void;
export const __wbindgen_export3: (a: number, b: number, c: number, d: number) => number;

View File

@@ -0,0 +1 @@
# Keep WASM files for npm

View File

@@ -0,0 +1,21 @@
{
"name": "ruvector-dag-wasm",
"collaborators": [
"RuVector Contributors"
],
"description": "Minimal WASM DAG library for browser and embedded systems",
"version": "0.1.0",
"license": "MIT OR Apache-2.0",
"files": [
"ruvector_dag_wasm_bg.wasm",
"ruvector_dag_wasm.js",
"ruvector_dag_wasm_bg.js",
"ruvector_dag_wasm.d.ts"
],
"module": "ruvector_dag_wasm.js",
"types": "ruvector_dag_wasm.d.ts",
"sideEffects": [
"./ruvector_dag_wasm.js",
"./snippets/*"
]
}

View File

@@ -0,0 +1,60 @@
/* tslint:disable */
/* eslint-disable */
export class WasmDag {
free(): void;
[Symbol.dispose](): void;
/**
* Get number of edges
*/
edge_count(): number;
/**
* Deserialize from bytes
*/
static from_bytes(data: Uint8Array): WasmDag;
/**
* Get number of nodes
*/
node_count(): number;
/**
* Find critical path (longest path by cost)
* Returns JSON: {"path": [node_ids], "cost": total}
*/
critical_path(): any;
/**
* Create new empty DAG
*/
constructor();
/**
* Serialize to JSON
*/
to_json(): string;
/**
* Add edge from -> to
* Returns false if creates cycle (simple check)
*/
add_edge(from: number, to: number): boolean;
/**
* Add a node with operator type and cost
* Returns node ID
*/
add_node(op: number, cost: number): number;
/**
* Serialize to bytes (bincode format)
*/
to_bytes(): Uint8Array;
/**
* Compute attention scores for nodes
* mechanism: 0=topological, 1=critical_path, 2=uniform
*/
attention(mechanism: number): Float32Array;
/**
* Deserialize from JSON
*/
static from_json(json: string): WasmDag;
/**
* Topological sort using Kahn's algorithm
* Returns node IDs in topological order
*/
topo_sort(): Uint32Array;
}

View File

@@ -0,0 +1,4 @@
import * as wasm from "./ruvector_dag_wasm_bg.wasm";
export * from "./ruvector_dag_wasm_bg.js";
import { __wbg_set_wasm } from "./ruvector_dag_wasm_bg.js";
__wbg_set_wasm(wasm);

View File

@@ -0,0 +1,370 @@
let wasm;
export function __wbg_set_wasm(val) {
wasm = val;
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function getArrayF32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
function getArrayU32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
function getArrayU8FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
let cachedFloat32ArrayMemory0 = null;
function getFloat32ArrayMemory0() {
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
}
return cachedFloat32ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return decodeText(ptr, len);
}
let cachedUint32ArrayMemory0 = null;
function getUint32ArrayMemory0() {
if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) {
cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer);
}
return cachedUint32ArrayMemory0;
}
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getObject(idx) { return heap[idx]; }
let heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1, 1) >>> 0;
getUint8ArrayMemory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = cachedTextEncoder.encodeInto(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
const MAX_SAFARI_DECODE_BYTES = 2146435072;
let numBytesDecoded = 0;
function decodeText(ptr, len) {
numBytesDecoded += len;
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
numBytesDecoded = len;
}
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
const cachedTextEncoder = new TextEncoder();
if (!('encodeInto' in cachedTextEncoder)) {
cachedTextEncoder.encodeInto = function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
}
}
let WASM_VECTOR_LEN = 0;
const WasmDagFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_wasmdag_free(ptr >>> 0, 1));
/**
* Minimal DAG structure for WASM
* Self-contained with no external dependencies beyond wasm-bindgen
*/
export class WasmDag {
static __wrap(ptr) {
ptr = ptr >>> 0;
const obj = Object.create(WasmDag.prototype);
obj.__wbg_ptr = ptr;
WasmDagFinalization.register(obj, obj.__wbg_ptr, obj);
return obj;
}
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
WasmDagFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_wasmdag_free(ptr, 0);
}
/**
* Get number of edges
* @returns {number}
*/
edge_count() {
const ret = wasm.wasmdag_edge_count(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Deserialize from bytes
* @param {Uint8Array} data
* @returns {WasmDag}
*/
static from_bytes(data) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm.wasmdag_from_bytes(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return WasmDag.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Get number of nodes
* @returns {number}
*/
node_count() {
const ret = wasm.wasmdag_node_count(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Find critical path (longest path by cost)
* Returns JSON: {"path": [node_ids], "cost": total}
* @returns {any}
*/
critical_path() {
const ret = wasm.wasmdag_critical_path(this.__wbg_ptr);
return takeObject(ret);
}
/**
* Create new empty DAG
*/
constructor() {
const ret = wasm.wasmdag_new();
this.__wbg_ptr = ret >>> 0;
WasmDagFinalization.register(this, this.__wbg_ptr, this);
return this;
}
/**
* Serialize to JSON
* @returns {string}
*/
to_json() {
let deferred1_0;
let deferred1_1;
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_to_json(retptr, this.__wbg_ptr);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
deferred1_0 = r0;
deferred1_1 = r1;
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_export2(deferred1_0, deferred1_1, 1);
}
}
/**
* Add edge from -> to
* Returns false if creates cycle (simple check)
* @param {number} from
* @param {number} to
* @returns {boolean}
*/
add_edge(from, to) {
const ret = wasm.wasmdag_add_edge(this.__wbg_ptr, from, to);
return ret !== 0;
}
/**
* Add a node with operator type and cost
* Returns node ID
* @param {number} op
* @param {number} cost
* @returns {number}
*/
add_node(op, cost) {
const ret = wasm.wasmdag_add_node(this.__wbg_ptr, op, cost);
return ret >>> 0;
}
/**
* Serialize to bytes (bincode format)
* @returns {Uint8Array}
*/
to_bytes() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_to_bytes(retptr, this.__wbg_ptr);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_export2(r0, r1 * 1, 1);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Compute attention scores for nodes
* mechanism: 0=topological, 1=critical_path, 2=uniform
* @param {number} mechanism
* @returns {Float32Array}
*/
attention(mechanism) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_attention(retptr, this.__wbg_ptr, mechanism);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v1 = getArrayF32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export2(r0, r1 * 4, 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Deserialize from JSON
* @param {string} json
* @returns {WasmDag}
*/
static from_json(json) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(json, wasm.__wbindgen_export, wasm.__wbindgen_export3);
const len0 = WASM_VECTOR_LEN;
wasm.wasmdag_from_json(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return WasmDag.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Topological sort using Kahn's algorithm
* Returns node IDs in topological order
* @returns {Uint32Array}
*/
topo_sort() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.wasmdag_topo_sort(retptr, this.__wbg_ptr);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var v1 = getArrayU32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export2(r0, r1 * 4, 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
}
if (Symbol.dispose) WasmDag.prototype[Symbol.dispose] = WasmDag.prototype.free;
export function __wbg___wbindgen_throw_dd24417ed36fc46e(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
export function __wbindgen_cast_2241b6af4c4b2941(arg0, arg1) {
// Cast intrinsic for `Ref(String) -> Externref`.
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};

Binary file not shown.

View File

@@ -0,0 +1,20 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const __wbg_wasmdag_free: (a: number, b: number) => void;
export const wasmdag_add_edge: (a: number, b: number, c: number) => number;
export const wasmdag_add_node: (a: number, b: number, c: number) => number;
export const wasmdag_attention: (a: number, b: number, c: number) => void;
export const wasmdag_critical_path: (a: number) => number;
export const wasmdag_edge_count: (a: number) => number;
export const wasmdag_from_bytes: (a: number, b: number, c: number) => void;
export const wasmdag_from_json: (a: number, b: number, c: number) => void;
export const wasmdag_new: () => number;
export const wasmdag_node_count: (a: number) => number;
export const wasmdag_to_bytes: (a: number, b: number) => void;
export const wasmdag_to_json: (a: number, b: number) => void;
export const wasmdag_topo_sort: (a: number, b: number) => void;
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
export const __wbindgen_export: (a: number, b: number) => number;
export const __wbindgen_export2: (a: number, b: number, c: number) => void;
export const __wbindgen_export3: (a: number, b: number, c: number, d: number) => number;

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":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAiB,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAwBhF;;GAEG;AACH,oBAAY,WAAW;IACrB,2BAA2B;IAC3B,IAAI,IAAI;IACR,0BAA0B;IAC1B,MAAM,IAAI;IACV,+BAA+B;IAC/B,OAAO,IAAI;IACX,qBAAqB;IACrB,IAAI,IAAI;IACR,6BAA6B;IAC7B,SAAS,IAAI;IACb,oBAAoB;IACpB,IAAI,IAAI;IACR,kBAAkB;IAClB,KAAK,IAAI;IACT,uBAAuB;IACvB,KAAK,IAAI;IACT,mCAAmC;IACnC,MAAM,MAAM;CACb;AAED;;GAEG;AACH,oBAAY,kBAAkB;IAC5B,6CAA6C;IAC7C,WAAW,IAAI;IACf,2CAA2C;IAC3C,aAAa,IAAI;IACjB,iCAAiC;IACjC,OAAO,IAAI;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,QAAQ,EAAE,WAAW,GAAG,MAAM,CAAC;IAC/B,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,UAAU,GAAG,aAAa,GAAG,IAAI,CAAC;IAC5C,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAkFD;;;;;;;;;;;GAWG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,IAAI,CAAC,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAC,CAA2B;IAG/C,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,MAAM,CAAQ;gBAEV,OAAO,GAAE,YAAiB;IAQtC;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B3B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAOlB;;OAEG;IACH,OAAO,CAAC,eAAe;IAQvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAMvB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IA4BjG;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IA0B1C;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;;OAGG;IACH,QAAQ,IAAI,MAAM,EAAE;IAUpB;;;;OAIG;IACH,YAAY,IAAI,YAAY;IA4B5B;;;;OAIG;IACH,SAAS,CAAC,SAAS,GAAE,kBAAqD,GAAG,MAAM,EAAE;IAQrF;;OAEG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAIxC;;OAEG;IACH,QAAQ,IAAI,OAAO,EAAE;IAIrB;;OAEG;IACH,OAAO,IAAI,UAAU;IAIrB;;OAEG;IACH,MAAM,IAAI,MAAM;IAIhB;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAcvC;;;;;;OAMG;WACU,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,aAAa,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IA4B1F;;;;;OAKG;WACU,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC;IA6BpF;;;;;OAKG;WACU,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC;IA6B/E;;;OAGG;WACU,UAAU,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,aAAa,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAenF;;;;;OAKG;WACU,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAmB7F;;;OAGG;WACU,YAAY,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,aAAa,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAe9G;;OAEG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,OAAO,IAAI,MAAM,GAAG,SAAS;IAI7B;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO3B;;;OAGG;IACH,OAAO,IAAI,IAAI;CAchB"}

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":"AAAA;;;;;;GAMG;AAMH,MAAM,WAAW,SAAS;IACxB,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAWD;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED;;;;GAIG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,GAAE,iBAAsB;IAK3C;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmD3B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAOlB;;;OAGG;IACG,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,SAAS,CAAC;IA2CjI;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA0CvI;;OAEG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAiBhD;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB1C;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAalC;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAcpD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAa5B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAM5D;;OAEG;IACH,KAAK,IAAI,IAAI;CAOd;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,WAAW,CAAS;IAEtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,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,SAAS,CAAC;IAqB3H,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAIjI,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAO1C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAOpC,IAAI,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAI5B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAI9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAM5D,KAAK,IAAI,IAAI;CAGd;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,GAAG,aAAa,CAKzF"}

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();
}

View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"outDir": "./dist",
"declaration": false,
"declarationMap": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "pkg", "pkg-node"]
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020", "DOM"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "pkg", "pkg-node"]
}