const WASM_PATHS = [
'../../pkg/edgevec.js', '/pkg/edgevec.js', '../pkg/edgevec.js', './pkg/edgevec.js' ];
let wasmModule = null;
export class SoftDeleteDemo {
constructor(dimension = 128, options = {}) {
if (!Number.isInteger(dimension) || dimension < 1 || dimension > 65536) {
throw new Error(`Invalid dimension: ${dimension}. Must be a positive integer between 1 and 65536.`);
}
this.dimension = dimension;
this.m = options.m || 16;
this.efConstruction = options.efConstruction || 200;
this.compactionThreshold = options.compactionThreshold || 0.3;
this.index = null;
this.insertedIds = [];
this.initialized = false;
this.listeners = {
insert: [],
delete: [],
compact: [],
search: [],
error: []
};
}
async initialize() {
if (this.initialized) {
return this;
}
if (!wasmModule) {
for (const path of WASM_PATHS) {
try {
console.log(`[SoftDeleteDemo] Trying WASM path: ${path}`);
wasmModule = await import(path);
console.log(`[SoftDeleteDemo] Loaded WASM from: ${path}`);
break;
} catch (e) {
console.warn(`[SoftDeleteDemo] Failed to load from ${path}: ${e.message}`);
}
}
if (!wasmModule) {
throw new Error('Could not load WASM module from any path');
}
await wasmModule.default();
}
const config = new wasmModule.EdgeVecConfig(this.dimension);
this.index = new wasmModule.EdgeVec(config);
this.initialized = true;
this._emit('insert', { type: 'init', dimension: this.dimension });
return this;
}
_ensureInitialized() {
if (!this.initialized || !this.index) {
throw new Error('SoftDeleteDemo not initialized. Call initialize() first.');
}
}
_emit(event, data) {
this.listeners[event]?.forEach(fn => fn(data));
}
on(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback);
}
return this;
}
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter(fn => fn !== callback);
}
return this;
}
insert(count) {
this._ensureInitialized();
const ids = [];
const start = performance.now();
for (let i = 0; i < count; i++) {
const vector = new Float32Array(this.dimension);
for (let j = 0; j < this.dimension; j++) {
vector[j] = Math.random() * 2 - 1; }
const id = this.index.insert(vector);
ids.push(id);
this.insertedIds.push(id);
}
const elapsed = performance.now() - start;
const result = {
count,
ids,
elapsed,
perVector: elapsed / count,
throughput: count / (elapsed / 1000)
};
this._emit('insert', result);
return result;
}
insertVector(vector) {
this._ensureInitialized();
if (vector.length !== this.dimension) {
throw new Error(`Dimension mismatch: expected ${this.dimension}, got ${vector.length}`);
}
const id = this.index.insert(vector);
this.insertedIds.push(id);
this._emit('insert', { count: 1, ids: [id] });
return id;
}
delete(id) {
this._ensureInitialized();
const result = this.index.softDelete(id);
if (result) {
this._emit('delete', { ids: [id], count: 1 });
}
return result;
}
deleteRandom(ratio) {
this._ensureInitialized();
if (ratio < 0 || ratio > 1) {
throw new Error('Ratio must be between 0.0 and 1.0');
}
const liveIds = this.insertedIds.filter(id => {
try {
return !this.index.isDeleted(id);
} catch (e) {
console.warn(`[SoftDeleteDemo] Could not check delete status for vector ${id}:`, e.message || e);
return false;
}
});
const toDelete = Math.floor(liveIds.length * ratio);
if (toDelete === 0) {
return { count: 0, ids: [], elapsed: 0 };
}
const shuffled = [...liveIds];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
const targets = shuffled.slice(0, toDelete);
const deleted = [];
const start = performance.now();
for (const id of targets) {
try {
if (this.index.softDelete(id)) {
deleted.push(id);
}
} catch (e) {
this._emit('error', { operation: 'delete', id, error: e });
}
}
const elapsed = performance.now() - start;
const result = {
count: deleted.length,
ids: deleted,
elapsed,
ratio: deleted.length / liveIds.length
};
this._emit('delete', result);
return result;
}
isDeleted(id) {
this._ensureInitialized();
return this.index.isDeleted(id);
}
search(query = null, k = 10) {
this._ensureInitialized();
if (!query) {
query = new Float32Array(this.dimension);
for (let i = 0; i < this.dimension; i++) {
query[i] = Math.random() * 2 - 1;
}
}
if (query.length !== this.dimension) {
throw new Error(`Query dimension mismatch: expected ${this.dimension}, got ${query.length}`);
}
const start = performance.now();
const results = this.index.search(query, k);
const elapsed = performance.now() - start;
const searchResult = {
results: Array.from(results).map(r => ({
id: r.id,
distance: r.score
})),
elapsed,
k,
returned: results.length
};
this._emit('search', searchResult);
return searchResult;
}
needsCompaction() {
this._ensureInitialized();
return this.index.needsCompaction();
}
getCompactionWarning() {
this._ensureInitialized();
return this.index.compactionWarning();
}
compact() {
this._ensureInitialized();
const start = performance.now();
const result = this.index.compact();
const elapsed = performance.now() - start;
this.insertedIds = this.insertedIds.filter(id => {
try {
return !this.index.isDeleted(id);
} catch (e) {
console.debug(`[SoftDeleteDemo] Vector ${id} no longer exists (expected after compaction)`);
return false;
}
});
const compactResult = {
tombstonesRemoved: result.tombstones_removed,
newSize: result.new_size,
durationMs: result.duration_ms,
totalElapsed: elapsed
};
this._emit('compact', compactResult);
return compactResult;
}
getStats() {
this._ensureInitialized();
const live = this.index.liveCount();
const deleted = this.index.deletedCount();
const total = live + deleted;
return {
total,
live,
deleted,
tombstoneRatio: this.index.tombstoneRatio(),
needsCompaction: this.index.needsCompaction(),
compactionWarning: this.index.compactionWarning(),
dimension: this.dimension,
memoryEstimateKB: Math.round(total * this.dimension * 4 / 1024)
};
}
getCompactionThreshold() {
this._ensureInitialized();
return this.index.compactionThreshold();
}
setCompactionThreshold(threshold) {
this._ensureInitialized();
this.index.setCompactionThreshold(threshold);
}
reset() {
this._ensureInitialized();
this.insertedIds = [];
const config = new wasmModule.EdgeVecConfig(this.dimension);
this.index = new wasmModule.EdgeVec(config);
this._emit('insert', { type: 'reset' });
}
dispose() {
if (this.index) {
this.index.free();
this.index = null;
}
this.initialized = false;
this.insertedIds = [];
}
async benchmark(vectorCount = 1000, deleteRatio = 0.3) {
this._ensureInitialized();
const results = {
vectorCount,
deleteRatio,
dimension: this.dimension,
phases: {}
};
const insertResult = this.insert(vectorCount);
results.phases.insert = {
elapsed: insertResult.elapsed,
throughput: insertResult.throughput
};
const searchBefore = this.search(null, 10);
results.phases.searchBeforeDelete = {
elapsed: searchBefore.elapsed,
resultsCount: searchBefore.returned
};
const deleteResult = this.deleteRandom(deleteRatio);
results.phases.delete = {
deleted: deleteResult.count,
elapsed: deleteResult.elapsed
};
const searchAfter = this.search(null, 10);
results.phases.searchAfterDelete = {
elapsed: searchAfter.elapsed,
resultsCount: searchAfter.returned
};
if (this.needsCompaction()) {
const compactResult = this.compact();
results.phases.compact = {
tombstonesRemoved: compactResult.tombstonesRemoved,
newSize: compactResult.newSize,
elapsed: compactResult.durationMs
};
}
const searchFinal = this.search(null, 10);
results.phases.searchAfterCompact = {
elapsed: searchFinal.elapsed,
resultsCount: searchFinal.returned
};
results.finalStats = this.getStats();
return results;
}
}
if (typeof window !== 'undefined') {
window.SoftDeleteDemo = SoftDeleteDemo;
}