ruvector-wasm 0.1.29

WASM bindings for Ruvector for browser deployment
Documentation
/**
 * IndexedDB Persistence Layer for Ruvector
 *
 * Provides:
 * - Save/load database state to IndexedDB
 * - Batch operations for performance
 * - Progressive loading with pagination
 * - LRU cache for hot vectors
 */

const DB_NAME = 'ruvector_storage';
const DB_VERSION = 1;
const VECTOR_STORE = 'vectors';
const META_STORE = 'metadata';

/**
 * LRU Cache for hot vectors
 */
class LRUCache {
  constructor(capacity = 1000) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) return null;

    // Move to end (most recently used)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);

    return value;
  }

  set(key, value) {
    // Remove if exists
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }

    // Add to end
    this.cache.set(key, value);

    // Evict oldest if over capacity
    if (this.cache.size > this.capacity) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }

  has(key) {
    return this.cache.has(key);
  }

  clear() {
    this.cache.clear();
  }

  get size() {
    return this.cache.size;
  }
}

/**
 * IndexedDB Persistence Manager
 */
export class IndexedDBPersistence {
  constructor(dbName = null) {
    this.dbName = dbName || DB_NAME;
    this.db = null;
    this.cache = new LRUCache(1000);
  }

  /**
   * Open IndexedDB connection
   */
  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, DB_VERSION);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        // Create object stores if they don't exist
        if (!db.objectStoreNames.contains(VECTOR_STORE)) {
          const vectorStore = db.createObjectStore(VECTOR_STORE, { keyPath: 'id' });
          vectorStore.createIndex('timestamp', 'timestamp', { unique: false });
        }

        if (!db.objectStoreNames.contains(META_STORE)) {
          db.createObjectStore(META_STORE, { keyPath: 'key' });
        }
      };
    });
  }

  /**
   * Save a single vector
   */
  async saveVector(id, vector, metadata = null) {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([VECTOR_STORE], 'readwrite');
      const store = transaction.objectStore(VECTOR_STORE);

      const data = {
        id,
        vector: Array.from(vector), // Convert Float32Array to regular array
        metadata,
        timestamp: Date.now()
      };

      const request = store.put(data);

      request.onsuccess = () => {
        this.cache.set(id, data);
        resolve(id);
      };
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Save vectors in batch (more efficient)
   */
  async saveBatch(entries, batchSize = 100) {
    if (!this.db) await this.open();

    const chunks = [];
    for (let i = 0; i < entries.length; i += batchSize) {
      chunks.push(entries.slice(i, i + batchSize));
    }

    for (const chunk of chunks) {
      await new Promise((resolve, reject) => {
        const transaction = this.db.transaction([VECTOR_STORE], 'readwrite');
        const store = transaction.objectStore(VECTOR_STORE);

        for (const entry of chunk) {
          const data = {
            id: entry.id,
            vector: Array.from(entry.vector),
            metadata: entry.metadata,
            timestamp: Date.now()
          };

          store.put(data);
          this.cache.set(entry.id, data);
        }

        transaction.oncomplete = () => resolve();
        transaction.onerror = () => reject(transaction.error);
      });
    }

    return entries.length;
  }

  /**
   * Load a single vector by ID
   */
  async loadVector(id) {
    // Check cache first
    if (this.cache.has(id)) {
      return this.cache.get(id);
    }

    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([VECTOR_STORE], 'readonly');
      const store = transaction.objectStore(VECTOR_STORE);
      const request = store.get(id);

      request.onsuccess = () => {
        const data = request.result;
        if (data) {
          // Convert array back to Float32Array
          data.vector = new Float32Array(data.vector);
          this.cache.set(id, data);
        }
        resolve(data);
      };
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Load all vectors (with progressive loading)
   */
  async loadAll(onProgress = null, batchSize = 100) {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([VECTOR_STORE], 'readonly');
      const store = transaction.objectStore(VECTOR_STORE);
      const request = store.openCursor();

      const vectors = [];
      let count = 0;

      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          const data = cursor.value;
          data.vector = new Float32Array(data.vector);
          vectors.push(data);
          count++;

          // Cache hot vectors (first 1000)
          if (count <= 1000) {
            this.cache.set(data.id, data);
          }

          // Report progress every batch
          if (onProgress && count % batchSize === 0) {
            onProgress({
              loaded: count,
              vectors: [...vectors]
            });
            vectors.length = 0; // Clear batch
          }

          cursor.continue();
        } else {
          // Done
          if (onProgress && vectors.length > 0) {
            onProgress({
              loaded: count,
              vectors: vectors,
              complete: true
            });
          }
          resolve({ count, complete: true });
        }
      };

      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Delete a vector by ID
   */
  async deleteVector(id) {
    if (!this.db) await this.open();

    this.cache.delete(id);

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([VECTOR_STORE], 'readwrite');
      const store = transaction.objectStore(VECTOR_STORE);
      const request = store.delete(id);

      request.onsuccess = () => resolve(true);
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Clear all vectors
   */
  async clear() {
    if (!this.db) await this.open();

    this.cache.clear();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([VECTOR_STORE], 'readwrite');
      const store = transaction.objectStore(VECTOR_STORE);
      const request = store.clear();

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Get database statistics
   */
  async getStats() {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([VECTOR_STORE], 'readonly');
      const store = transaction.objectStore(VECTOR_STORE);
      const request = store.count();

      request.onsuccess = () => {
        resolve({
          totalVectors: request.result,
          cacheSize: this.cache.size,
          cacheHitRate: this.cache.size / request.result
        });
      };
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Save metadata
   */
  async saveMeta(key, value) {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([META_STORE], 'readwrite');
      const store = transaction.objectStore(META_STORE);
      const request = store.put({ key, value });

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Load metadata
   */
  async loadMeta(key) {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([META_STORE], 'readonly');
      const store = transaction.objectStore(META_STORE);
      const request = store.get(key);

      request.onsuccess = () => {
        const data = request.result;
        resolve(data ? data.value : null);
      };
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Close the database connection
   */
  close() {
    if (this.db) {
      this.db.close();
      this.db = null;
    }
  }
}

export default IndexedDBPersistence;