const DB_NAME = 'ruvector_storage';
const DB_VERSION = 1;
const VECTOR_STORE = 'vectors';
const META_STORE = 'metadata';
class LRUCache {
constructor(capacity = 1000) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
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;
}
}
export class IndexedDBPersistence {
constructor(dbName = null) {
this.dbName = dbName || DB_NAME;
this.db = null;
this.cache = new LRUCache(1000);
}
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;
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' });
}
};
});
}
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), metadata,
timestamp: Date.now()
};
const request = store.put(data);
request.onsuccess = () => {
this.cache.set(id, data);
resolve(id);
};
request.onerror = () => reject(request.error);
});
}
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;
}
async loadVector(id) {
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) {
data.vector = new Float32Array(data.vector);
this.cache.set(id, data);
}
resolve(data);
};
request.onerror = () => reject(request.error);
});
}
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++;
if (count <= 1000) {
this.cache.set(data.id, data);
}
if (onProgress && count % batchSize === 0) {
onProgress({
loaded: count,
vectors: [...vectors]
});
vectors.length = 0; }
cursor.continue();
} else {
if (onProgress && vectors.length > 0) {
onProgress({
loaded: count,
vectors: vectors,
complete: true
});
}
resolve({ count, complete: true });
}
};
request.onerror = () => reject(request.error);
});
}
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);
});
}
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);
});
}
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);
});
}
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);
});
}
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() {
if (this.db) {
this.db.close();
this.db = null;
}
}
}
export default IndexedDBPersistence;