@claude-flow/embeddings
Version:
V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking
153 lines • 5.04 kB
JavaScript
/**
* Base classes for the embedding-service module — extracted from
* embedding-service.ts so sibling provider files (e.g.
* ruvector-onnx-embedding-service.ts, future plugin providers) can
* extend the base without causing a circular-import TDZ.
*
* The split happened during ADR-121 Phase 1. Before the split, every
* concrete provider lived in embedding-service.ts; introducing a new
* provider in a separate file that also imported `BaseEmbeddingService`
* from embedding-service.ts created a cycle that crashed at module
* load with "Class extends undefined".
*
* This file has no concrete provider classes — only the abstract base
* + the LRU cache it uses. That keeps the dependency graph from any
* provider back to base, and never the reverse.
*/
import { EventEmitter } from 'events';
import { normalize } from './normalization.js';
import { PersistentEmbeddingCache } from './persistent-cache.js';
// ============================================================================
// LRU Cache Implementation
// ============================================================================
export class LRUCache {
maxSize;
cache = new Map();
hits = 0;
misses = 0;
constructor(maxSize) {
this.maxSize = maxSize;
}
get(key) {
const value = this.cache.get(key);
if (value !== undefined) {
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, value);
this.hits++;
return value;
}
this.misses++;
return undefined;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
}
clear() {
this.cache.clear();
this.hits = 0;
this.misses = 0;
}
get size() {
return this.cache.size;
}
get hitRate() {
const total = this.hits + this.misses;
return total > 0 ? this.hits / total : 0;
}
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
hits: this.hits,
misses: this.misses,
hitRate: this.hitRate,
};
}
}
// ============================================================================
// Base Embedding Service
// ============================================================================
export class BaseEmbeddingService extends EventEmitter {
config;
cache;
persistentCache = null;
embeddingListeners = new Set();
normalizationType;
constructor(config) {
super();
this.config = config;
this.cache = new LRUCache(config.cacheSize ?? 1000);
this.normalizationType = config.normalization ?? 'none';
if (config.persistentCache?.enabled) {
const pcConfig = config.persistentCache;
this.persistentCache = new PersistentEmbeddingCache({
dbPath: pcConfig.dbPath ?? '.cache/embeddings.db',
maxSize: pcConfig.maxSize ?? 10000,
ttlMs: pcConfig.ttlMs,
});
}
}
/** Apply normalization to embedding if configured. */
applyNormalization(embedding) {
if (this.normalizationType === 'none')
return embedding;
return normalize(embedding, { type: this.normalizationType });
}
/** Check persistent cache for embedding. */
async checkPersistentCache(text) {
if (!this.persistentCache)
return null;
return this.persistentCache.get(text);
}
/** Store embedding in persistent cache. */
async storePersistentCache(text, embedding) {
if (!this.persistentCache)
return;
await this.persistentCache.set(text, embedding);
}
emitEvent(event) {
for (const listener of this.embeddingListeners) {
try {
listener(event);
}
catch (error) {
console.error('Error in embedding event listener:', error);
}
}
this.emit(event.type, event);
}
addEventListener(listener) {
this.embeddingListeners.add(listener);
}
removeEventListener(listener) {
this.embeddingListeners.delete(listener);
}
clearCache() {
const size = this.cache.size;
this.cache.clear();
this.emitEvent({ type: 'cache_eviction', size });
}
getCacheStats() {
const stats = this.cache.getStats();
return {
size: stats.size,
maxSize: stats.maxSize,
hitRate: stats.hitRate,
};
}
async shutdown() {
this.clearCache();
this.embeddingListeners.clear();
}
}
//# sourceMappingURL=embedding-service-base.js.map