mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
dc8463872d
* docs: add ADR-057 — RVF native storage backend to replace sql.js Proposes replacing the 18MB sql.js WASM SQLite dependency with RuVector Format (RVF) across @claude-flow/shared, memory, and embeddings packages. Includes automatic and manual migration paths for existing .db and .json files with full backward compatibility. Key benefits: -18MB install size, 150x-12,500x faster vector search via native HNSW, quantization support, crash-safe append-only format. Co-Authored-By: claude-flow <ruv@ruv.net> * docs: extend ADR-057 with RVF embedding provider, ruvLLM persistence, and progressive download Add three major sections to ADR-057: - Section 3A: RVF as 5th EmbeddingProvider (52KB WASM replaces 540MB agentic-flow for basic use cases). Updated auto-select hierarchy: rvf > agentic-flow > transformers > mock. - Section 3B: ruvLLM learning persistence via RVF segments. SONA patterns (ReasoningBank), LoRA adapters, and EWC++ weights now survive process restarts. RuVectorProvider.searchMemory works offline via RVF without HTTP server. - Section 10: Progressive download architecture. Replaces npm optionalDependencies (all-or-nothing 1.3GB) with on-demand capability downloads stored as .rvf files. 5 phases from 22MB core CLI to full LLM/CNN/GNN/kernel capabilities. Implementation plan expanded from 5 to 8 phases. Testing strategy updated with 11 new test cases covering the additions. Co-Authored-By: claude-flow <ruv@ruv.net> * feat: add optimized Docker images for Ruflo lite/full/workers - Dockerfile: Multi-stage Alpine build with aggressive dependency pruning. Result: 324MB image, 0.6s startup (vs 1.3GB/35s baseline). Uses --omit=optional + explicit rm -rf of heavy transitive deps. - Dockerfile.full: Full image variant with all optional dependencies including native compilation (onnxruntime-node, better-sqlite3). - Dockerfile.headless: Updated from node:20 to node:22, claude-flow branding to ruflo, same prune strategy as lite. - docker-compose.yml: Three profiles (lite, full, workers) with appropriate resource limits and health checks. - .dockerignore: Excludes node_modules, tests, source maps, IDE configs, and secrets from build context. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(ADR-057): implement RVF native storage backend across all packages Replace sql.js (18MB WASM) with zero-dependency RVF format across memory, events, embeddings, and learning subsystems. Pure TypeScript fallback with optional native @ruvector/rvf acceleration. New files: - memory/rvf-backend.ts: IMemoryBackend impl with HNSW + binary persistence - memory/hnsw-lite.ts: Lightweight HNSW graph for vector search - memory/rvf-learning-store.ts: SONA pattern/LoRA/EWC/trajectory persistence - memory/persistent-sona.ts: PersistentSonaCoordinator wrapping learning store - shared/events/rvf-event-log.ts: Append-only event log (replaces EventStore) - embeddings/rvf-embedding-cache.ts: Binary file embedding cache (FNV-1a keys) - embeddings/rvf-embedding-service.ts: Hash-based embedding provider (<0.1ms) Integrations: - DatabaseProvider: 'rvf' added, auto-selected first (always available) - EmbeddingProvider: 'rvf' added, auto-selected first in async factory - All packages export new RVF types and classes Tests: 163 passing (5 suites), validated in Docker (node:22-alpine) - rvf-backend: 50 tests (CRUD, query, search, persistence, HNSW) - rvf-event-log: 33 tests (append, replay, snapshots, crash recovery) - rvf-embeddings: 30 tests (cache, service, determinism, normalization) - rvf-learning-store: 31 tests (patterns, LoRA, EWC, trajectories) - rvf-integration: 19 tests (provider selection, migration, concurrency) Co-Authored-By: claude-flow <ruv@ruv.net> * fix(ADR-057): harden RVF storage with security, performance, and migration Security: atomic writes (tmp+rename) across all backends, path validation, input validation at boundaries. Performance: persist lock prevents concurrent flushes, stack overflow fix for large datasets, timer.unref() prevents hangs, O(1) eviction lookup in embedding cache. New: bidirectional RVF migration utility (JSON/SQLite/RVF), format v2 with backward compat, auto-persist patterns and trajectories before eviction. Added HnswLite and RvfMigrator exports. 193 tests passing, 0 TS errors. Refs #1242 Co-Authored-By: claude-flow <ruv@ruv.net> * fix: doctor version detection and init settings warnings (#1235, #1230) doctor: Replace unreliable path-walking heuristic with import.meta.url traversal to find the correct package.json. Removes stale npx cache scanning that could report wrong versions. Fixes #1235. init: Remove invalid TeammateIdle and TaskCompleted keys from generated .claude/settings.json hooks section. These are not recognized by Claude Code and caused warnings. Config remains available under the claudeFlow namespace. Fixes #1230. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(doctor): remove Windows-incompatible shell redirects, simplify root detection - Remove fragile Windows root calculation (split on backslash); use dirname(dir) === dir instead which works on all platforms - Remove 2>/dev/null from npm view command (fails on Windows cmd.exe); the catch block handles errors gracefully already Co-Authored-By: claude-flow <ruv@ruv.net> * security: harden RVF against crafted files and race conditions - rvf-backend: validate dimensions (1-10000), validate RVF header structure and size before JSON.parse, guard persist interval against concurrent flushes - rvf-embedding-cache: use random suffix for tmp files to prevent predictable-path attacks on multi-user systems - rvf-event-log: add 100MB max payload size limit to prevent DoS from crafted event log files - rvf-migration: add null byte and type validation on all paths Co-Authored-By: claude-flow <ruv@ruv.net>
579 lines
16 KiB
TypeScript
579 lines
16 KiB
TypeScript
/**
|
|
* RVF Embedding Cache - Pure TypeScript Binary File Cache
|
|
*
|
|
* Replaces the sql.js-based PersistentEmbeddingCache with a lightweight
|
|
* pure-TS binary file format. No native dependencies required.
|
|
*
|
|
* Features:
|
|
* - Map-based in-memory cache with periodic flush to binary file
|
|
* - LRU eviction tracked via access timestamps
|
|
* - TTL support for cache entries
|
|
* - Deterministic FNV-1a text hashing for keys
|
|
* - Binary format: RVEC magic + entry records
|
|
*
|
|
* Binary entry format:
|
|
* [4-byte key-hash][4-byte dims][dims*4 bytes float32][8-byte timestamp][8-byte access-count]
|
|
*
|
|
* @module @claude-flow/embeddings
|
|
*/
|
|
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
import { dirname } from 'path';
|
|
|
|
/** Validate a file path is safe */
|
|
function validatePath(p: string): void {
|
|
if (p.includes('\0')) throw new Error('Cache path contains null bytes');
|
|
}
|
|
|
|
// ============================================================================
|
|
// Configuration
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Configuration for RVF embedding cache
|
|
*/
|
|
export interface RvfEmbeddingCacheConfig {
|
|
/** Path to the binary cache file */
|
|
cachePath: string;
|
|
/** Maximum number of entries (default: 10000) */
|
|
maxSize?: number;
|
|
/** TTL in milliseconds (default: 7 days) */
|
|
ttlMs?: number;
|
|
/** Embedding dimensions (used for validation) */
|
|
dimensions?: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Cache Entry
|
|
// ============================================================================
|
|
|
|
interface CacheEntry {
|
|
embedding: Float32Array;
|
|
createdAt: number;
|
|
accessedAt: number;
|
|
accessCount: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Constants
|
|
// ============================================================================
|
|
|
|
/** Binary file magic bytes: "RVEC" */
|
|
const MAGIC = new Uint8Array([0x52, 0x56, 0x45, 0x43]); // R V E C
|
|
|
|
/** Default TTL: 7 days in milliseconds */
|
|
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
|
/** Default max entries */
|
|
const DEFAULT_MAX_SIZE = 10000;
|
|
|
|
/** Auto-flush interval: 30 seconds */
|
|
const AUTO_FLUSH_INTERVAL_MS = 30000;
|
|
|
|
/** FNV-1a offset basis (32-bit) */
|
|
const FNV_OFFSET_BASIS = 0x811c9dc5;
|
|
|
|
/** FNV-1a prime (32-bit) */
|
|
const FNV_PRIME = 0x01000193;
|
|
|
|
// ============================================================================
|
|
// RVF Embedding Cache
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Pure-TS binary file embedding cache with LRU eviction and TTL support.
|
|
*
|
|
* Stores embeddings as raw Float32Array bytes keyed by FNV-1a text hashes.
|
|
* Uses an in-memory Map with periodic flush to a compact binary file.
|
|
*/
|
|
export class RvfEmbeddingCache {
|
|
private readonly cachePath: string;
|
|
private readonly maxSize: number;
|
|
private readonly ttlMs: number;
|
|
private readonly dimensions: number | undefined;
|
|
|
|
private entries: Map<number, CacheEntry> = new Map();
|
|
private textToHash: Map<string, number> = new Map();
|
|
private dirty = false;
|
|
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
private initialized = false;
|
|
|
|
constructor(config: RvfEmbeddingCacheConfig) {
|
|
this.cachePath = config.cachePath;
|
|
this.maxSize = config.maxSize ?? DEFAULT_MAX_SIZE;
|
|
this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
|
|
this.dimensions = config.dimensions;
|
|
validatePath(this.cachePath);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Initialization
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Lazily initialize the cache: load from disk if file exists, start auto-flush.
|
|
*/
|
|
private async ensureInitialized(): Promise<void> {
|
|
if (this.initialized) return;
|
|
|
|
// Ensure parent directory exists
|
|
const dir = dirname(this.cachePath);
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
// Load existing cache file
|
|
if (existsSync(this.cachePath)) {
|
|
this.loadFromFile();
|
|
}
|
|
|
|
// Clean expired entries on startup
|
|
this.cleanExpired();
|
|
|
|
// Start auto-flush timer
|
|
this.startAutoFlush();
|
|
|
|
this.initialized = true;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Public API (matches PersistentEmbeddingCache)
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Get an embedding from the cache by text.
|
|
* Returns null if not found or expired.
|
|
*/
|
|
async get(text: string): Promise<Float32Array | null> {
|
|
await this.ensureInitialized();
|
|
|
|
const hash = this.hashText(text);
|
|
const entry = this.entries.get(hash);
|
|
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
|
|
// Check TTL
|
|
const now = Date.now();
|
|
if (now - entry.createdAt > this.ttlMs) {
|
|
this.entries.delete(hash);
|
|
this.textToHash.delete(text);
|
|
this.dirty = true;
|
|
return null;
|
|
}
|
|
|
|
// Update LRU tracking
|
|
entry.accessedAt = now;
|
|
entry.accessCount++;
|
|
this.dirty = true;
|
|
|
|
return entry.embedding;
|
|
}
|
|
|
|
/**
|
|
* Store an embedding in the cache.
|
|
* Triggers LRU eviction if the cache exceeds maxSize.
|
|
*/
|
|
async set(text: string, embedding: Float32Array): Promise<void> {
|
|
await this.ensureInitialized();
|
|
|
|
// Validate dimensions if configured
|
|
if (this.dimensions !== undefined && embedding.length !== this.dimensions) {
|
|
throw new Error(
|
|
`Dimension mismatch: expected ${this.dimensions}, got ${embedding.length}`
|
|
);
|
|
}
|
|
|
|
const hash = this.hashText(text);
|
|
const now = Date.now();
|
|
|
|
// Copy the embedding to avoid external mutation
|
|
const copy = new Float32Array(embedding.length);
|
|
copy.set(embedding);
|
|
|
|
const existing = this.entries.get(hash);
|
|
if (existing) {
|
|
existing.embedding = copy;
|
|
existing.accessedAt = now;
|
|
existing.accessCount++;
|
|
} else {
|
|
this.entries.set(hash, {
|
|
embedding: copy,
|
|
createdAt: now,
|
|
accessedAt: now,
|
|
accessCount: 1,
|
|
});
|
|
this.textToHash.set(text, hash);
|
|
}
|
|
|
|
this.dirty = true;
|
|
|
|
// Evict if over capacity
|
|
this.evictIfNeeded();
|
|
}
|
|
|
|
/**
|
|
* Check whether the cache contains an embedding for the given text.
|
|
*/
|
|
async has(text: string): Promise<boolean> {
|
|
await this.ensureInitialized();
|
|
|
|
const hash = this.hashText(text);
|
|
const entry = this.entries.get(hash);
|
|
|
|
if (!entry) return false;
|
|
|
|
// Check TTL
|
|
if (Date.now() - entry.createdAt > this.ttlMs) {
|
|
this.entries.delete(hash);
|
|
this.textToHash.delete(text);
|
|
this.dirty = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Delete a specific entry from the cache.
|
|
* Returns true if the entry existed and was deleted.
|
|
*/
|
|
async delete(text: string): Promise<boolean> {
|
|
await this.ensureInitialized();
|
|
|
|
const hash = this.hashText(text);
|
|
const existed = this.entries.delete(hash);
|
|
this.textToHash.delete(text);
|
|
|
|
if (existed) {
|
|
this.dirty = true;
|
|
}
|
|
|
|
return existed;
|
|
}
|
|
|
|
/**
|
|
* Clear all entries from the cache and persist the empty state.
|
|
*/
|
|
async clear(): Promise<void> {
|
|
await this.ensureInitialized();
|
|
|
|
this.entries.clear();
|
|
this.textToHash.clear();
|
|
this.dirty = true;
|
|
this.flushToFile();
|
|
}
|
|
|
|
/**
|
|
* Return the number of entries currently in the cache.
|
|
*/
|
|
async size(): Promise<number> {
|
|
await this.ensureInitialized();
|
|
return this.entries.size;
|
|
}
|
|
|
|
/**
|
|
* Flush pending changes to disk and stop the auto-flush timer.
|
|
*/
|
|
async close(): Promise<void> {
|
|
this.stopAutoFlush();
|
|
|
|
if (this.dirty) {
|
|
this.flushToFile();
|
|
}
|
|
|
|
this.entries.clear();
|
|
this.textToHash.clear();
|
|
this.initialized = false;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Hashing
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* FNV-1a 32-bit hash of the input text.
|
|
* Deterministic: same input always produces the same hash.
|
|
*/
|
|
private hashText(text: string): number {
|
|
let hash = FNV_OFFSET_BASIS;
|
|
for (let i = 0; i < text.length; i++) {
|
|
hash ^= text.charCodeAt(i);
|
|
hash = Math.imul(hash, FNV_PRIME) >>> 0;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// LRU Eviction
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* If the cache exceeds maxSize, evict the least-recently-accessed entries
|
|
* until we are back at 90% capacity.
|
|
*/
|
|
private evictIfNeeded(): void {
|
|
if (this.entries.size <= this.maxSize) return;
|
|
|
|
const targetSize = Math.floor(this.maxSize * 0.9);
|
|
const toEvict = this.entries.size - targetSize;
|
|
|
|
// Sort entries by accessedAt ascending (oldest first)
|
|
const sorted = [...this.entries.entries()].sort(
|
|
(a, b) => a[1].accessedAt - b[1].accessedAt
|
|
);
|
|
|
|
// Build reverse map for O(1) lookup (hash → text)
|
|
const hashToText = new Map<number, string>();
|
|
for (const [text, hash] of this.textToHash) {
|
|
hashToText.set(hash, text);
|
|
}
|
|
|
|
for (let i = 0; i < toEvict && i < sorted.length; i++) {
|
|
const [hash] = sorted[i];
|
|
this.entries.delete(hash);
|
|
const text = hashToText.get(hash);
|
|
if (text !== undefined) this.textToHash.delete(text);
|
|
}
|
|
|
|
this.dirty = true;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TTL Cleanup
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Remove all entries whose createdAt timestamp is older than TTL.
|
|
*/
|
|
private cleanExpired(): void {
|
|
const cutoff = Date.now() - this.ttlMs;
|
|
const toDelete: number[] = [];
|
|
|
|
for (const [hash, entry] of this.entries) {
|
|
if (entry.createdAt < cutoff) {
|
|
toDelete.push(hash);
|
|
}
|
|
}
|
|
|
|
// Build reverse map for O(1) lookup
|
|
const hashToText = new Map<number, string>();
|
|
for (const [text, h] of this.textToHash) {
|
|
hashToText.set(h, text);
|
|
}
|
|
|
|
for (const hash of toDelete) {
|
|
this.entries.delete(hash);
|
|
const text = hashToText.get(hash);
|
|
if (text !== undefined) this.textToHash.delete(text);
|
|
}
|
|
|
|
if (toDelete.length > 0) {
|
|
this.dirty = true;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Auto-Flush Timer
|
|
// --------------------------------------------------------------------------
|
|
|
|
private startAutoFlush(): void {
|
|
if (this.flushTimer) return;
|
|
|
|
this.flushTimer = setInterval(() => {
|
|
if (this.dirty) {
|
|
this.flushToFile();
|
|
}
|
|
}, AUTO_FLUSH_INTERVAL_MS);
|
|
if (this.flushTimer.unref) this.flushTimer.unref();
|
|
}
|
|
|
|
private stopAutoFlush(): void {
|
|
if (this.flushTimer) {
|
|
clearInterval(this.flushTimer);
|
|
this.flushTimer = null;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Binary Serialization
|
|
// --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Write all entries to the binary cache file.
|
|
*
|
|
* Format:
|
|
* [4-byte magic "RVEC"]
|
|
* For each entry:
|
|
* [4-byte key-hash (uint32)]
|
|
* [4-byte dims (uint32)]
|
|
* [dims * 4 bytes float32 data]
|
|
* [8-byte createdAt (float64, used as timestamp)]
|
|
* [8-byte accessCount (float64)]
|
|
*/
|
|
private flushToFile(): void {
|
|
try {
|
|
// Version 2 format: magic(4) + version(4) + entries...
|
|
// Entry: hash(4) + dims(4) + embedding(dims*4) + createdAt(8) + accessedAt(8) + accessCount(8)
|
|
let totalSize = MAGIC.length + 4; // magic + version uint32
|
|
for (const [, entry] of this.entries) {
|
|
totalSize += 4 + 4 + entry.embedding.length * 4 + 8 + 8 + 8;
|
|
}
|
|
|
|
const buffer = new ArrayBuffer(totalSize);
|
|
const view = new DataView(buffer);
|
|
const bytes = new Uint8Array(buffer);
|
|
let offset = 0;
|
|
|
|
// Write magic
|
|
bytes.set(MAGIC, 0);
|
|
offset += MAGIC.length;
|
|
|
|
// Write format version
|
|
view.setUint32(offset, 2, true);
|
|
offset += 4;
|
|
|
|
// Write entries
|
|
for (const [hash, entry] of this.entries) {
|
|
// Key hash (uint32, little-endian)
|
|
view.setUint32(offset, hash, true);
|
|
offset += 4;
|
|
|
|
// Dimensions (uint32, little-endian)
|
|
view.setUint32(offset, entry.embedding.length, true);
|
|
offset += 4;
|
|
|
|
// Embedding data (float32 array, little-endian)
|
|
for (let i = 0; i < entry.embedding.length; i++) {
|
|
view.setFloat32(offset, entry.embedding[i], true);
|
|
offset += 4;
|
|
}
|
|
|
|
// createdAt as float64 (little-endian) - v2: separate from accessedAt
|
|
view.setFloat64(offset, entry.createdAt, true);
|
|
offset += 8;
|
|
|
|
// accessedAt as float64 (little-endian)
|
|
view.setFloat64(offset, entry.accessedAt, true);
|
|
offset += 8;
|
|
|
|
// Access count as float64 (little-endian)
|
|
view.setFloat64(offset, entry.accessCount, true);
|
|
offset += 8;
|
|
}
|
|
|
|
// Ensure parent directory exists
|
|
const dir = dirname(this.cachePath);
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
const tmpSuffix = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
const tmpPath = this.cachePath + '.tmp.' + tmpSuffix;
|
|
writeFileSync(tmpPath, Buffer.from(buffer));
|
|
renameSync(tmpPath, this.cachePath);
|
|
this.dirty = false;
|
|
} catch (error) {
|
|
console.error(
|
|
'[rvf-embedding-cache] Flush error:',
|
|
error instanceof Error ? error.message : error
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load entries from the binary cache file.
|
|
*/
|
|
private loadFromFile(): void {
|
|
try {
|
|
const fileBuffer = readFileSync(this.cachePath);
|
|
if (fileBuffer.length < MAGIC.length) return;
|
|
|
|
const buffer = fileBuffer.buffer.slice(
|
|
fileBuffer.byteOffset,
|
|
fileBuffer.byteOffset + fileBuffer.byteLength
|
|
);
|
|
const view = new DataView(buffer);
|
|
const bytes = new Uint8Array(buffer);
|
|
let offset = 0;
|
|
|
|
// Verify magic
|
|
for (let i = 0; i < MAGIC.length; i++) {
|
|
if (bytes[offset + i] !== MAGIC[i]) {
|
|
console.warn('[rvf-embedding-cache] Invalid magic bytes, skipping load');
|
|
return;
|
|
}
|
|
}
|
|
offset += MAGIC.length;
|
|
|
|
// Check for version header (v2+)
|
|
let formatVersion = 1;
|
|
if (offset + 4 <= buffer.byteLength) {
|
|
const possibleVersion = view.getUint32(offset, true);
|
|
if (possibleVersion === 2) {
|
|
formatVersion = possibleVersion;
|
|
offset += 4;
|
|
}
|
|
}
|
|
|
|
// Read entries
|
|
while (offset + 8 <= buffer.byteLength) {
|
|
// Need at least 4 (hash) + 4 (dims) = 8 bytes for the header
|
|
const hash = view.getUint32(offset, true);
|
|
offset += 4;
|
|
|
|
const dims = view.getUint32(offset, true);
|
|
offset += 4;
|
|
|
|
const entryDataSize = formatVersion === 2
|
|
? dims * 4 + 8 + 8 + 8 // v2: embedding + createdAt + accessedAt + accessCount
|
|
: dims * 4 + 8 + 8; // v1: embedding + accessedAt + accessCount
|
|
if (offset + entryDataSize > buffer.byteLength) {
|
|
console.warn('[rvf-embedding-cache] Truncated entry, stopping load');
|
|
break;
|
|
}
|
|
|
|
// Read embedding
|
|
const embedding = new Float32Array(dims);
|
|
for (let i = 0; i < dims; i++) {
|
|
embedding[i] = view.getFloat32(offset, true);
|
|
offset += 4;
|
|
}
|
|
|
|
let createdAt: number;
|
|
let accessedAt: number;
|
|
let accessCount: number;
|
|
|
|
if (formatVersion >= 2) {
|
|
createdAt = view.getFloat64(offset, true);
|
|
offset += 8;
|
|
accessedAt = view.getFloat64(offset, true);
|
|
offset += 8;
|
|
accessCount = view.getFloat64(offset, true);
|
|
offset += 8;
|
|
} else {
|
|
// v1: only accessedAt was stored, use it as createdAt too
|
|
accessedAt = view.getFloat64(offset, true);
|
|
offset += 8;
|
|
accessCount = view.getFloat64(offset, true);
|
|
offset += 8;
|
|
createdAt = accessedAt;
|
|
}
|
|
|
|
this.entries.set(hash, {
|
|
embedding,
|
|
createdAt,
|
|
accessedAt,
|
|
accessCount,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
'[rvf-embedding-cache] Load error:',
|
|
error instanceof Error ? error.message : error
|
|
);
|
|
}
|
|
}
|
|
}
|