@cmdoss/memwal-sdk 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-sdk/PDWVectorStore.d.ts.map +1 -1
- package/dist/ai-sdk/PDWVectorStore.js +4 -1
- package/dist/ai-sdk/PDWVectorStore.js.map +1 -1
- package/dist/ai-sdk/tools.d.ts +2 -2
- package/dist/ai-sdk/tools.js +2 -2
- package/dist/browser.d.ts +5 -6
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +7 -6
- package/dist/browser.js.map +1 -1
- package/dist/client/ClientMemoryManager.d.ts +1 -0
- package/dist/client/ClientMemoryManager.d.ts.map +1 -1
- package/dist/client/ClientMemoryManager.js +5 -1
- package/dist/client/ClientMemoryManager.js.map +1 -1
- package/dist/client/SimplePDWClient.d.ts +24 -1
- package/dist/client/SimplePDWClient.d.ts.map +1 -1
- package/dist/client/SimplePDWClient.js +31 -9
- package/dist/client/SimplePDWClient.js.map +1 -1
- package/dist/client/namespaces/EmbeddingsNamespace.d.ts +1 -1
- package/dist/client/namespaces/EmbeddingsNamespace.js +1 -1
- package/dist/client/namespaces/IndexNamespace.d.ts +38 -9
- package/dist/client/namespaces/IndexNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/IndexNamespace.js +77 -10
- package/dist/client/namespaces/IndexNamespace.js.map +1 -1
- package/dist/client/namespaces/MemoryNamespace.d.ts +27 -0
- package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/MemoryNamespace.js +104 -0
- package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
- package/dist/client/namespaces/SearchNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/SearchNamespace.js +25 -14
- package/dist/client/namespaces/SearchNamespace.js.map +1 -1
- package/dist/client/namespaces/consolidated/AINamespace.d.ts +2 -2
- package/dist/client/namespaces/consolidated/AINamespace.js +2 -2
- package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/consolidated/BlockchainNamespace.js +69 -1
- package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
- package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +46 -0
- package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/consolidated/StorageNamespace.js +34 -0
- package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
- package/dist/graph/GraphService.js +2 -2
- package/dist/graph/GraphService.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/permissions/ConsentRepository.browser.d.ts +56 -0
- package/dist/permissions/ConsentRepository.browser.d.ts.map +1 -0
- package/dist/permissions/ConsentRepository.browser.js +198 -0
- package/dist/permissions/ConsentRepository.browser.js.map +1 -0
- package/dist/retrieval/MemoryRetrievalService.d.ts +31 -0
- package/dist/retrieval/MemoryRetrievalService.d.ts.map +1 -1
- package/dist/retrieval/MemoryRetrievalService.js +44 -4
- package/dist/retrieval/MemoryRetrievalService.js.map +1 -1
- package/dist/services/EmbeddingService.d.ts +28 -1
- package/dist/services/EmbeddingService.d.ts.map +1 -1
- package/dist/services/EmbeddingService.js +54 -0
- package/dist/services/EmbeddingService.js.map +1 -1
- package/dist/services/GeminiAIService.d.ts.map +1 -1
- package/dist/services/GeminiAIService.js +283 -27
- package/dist/services/GeminiAIService.js.map +1 -1
- package/dist/services/IndexManager.d.ts +5 -1
- package/dist/services/IndexManager.d.ts.map +1 -1
- package/dist/services/IndexManager.js +17 -40
- package/dist/services/IndexManager.js.map +1 -1
- package/dist/services/MemoryIndexService.d.ts +31 -2
- package/dist/services/MemoryIndexService.d.ts.map +1 -1
- package/dist/services/MemoryIndexService.js +75 -3
- package/dist/services/MemoryIndexService.js.map +1 -1
- package/dist/services/QueryService.js +1 -1
- package/dist/services/QueryService.js.map +1 -1
- package/dist/services/StorageService.d.ts +10 -0
- package/dist/services/StorageService.d.ts.map +1 -1
- package/dist/services/StorageService.js +13 -0
- package/dist/services/StorageService.js.map +1 -1
- package/dist/services/storage/QuiltBatchManager.d.ts +111 -4
- package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
- package/dist/services/storage/QuiltBatchManager.js +450 -38
- package/dist/services/storage/QuiltBatchManager.js.map +1 -1
- package/dist/services/storage/index.d.ts +1 -1
- package/dist/services/storage/index.d.ts.map +1 -1
- package/dist/services/storage/index.js.map +1 -1
- package/dist/utils/LRUCache.d.ts +106 -0
- package/dist/utils/LRUCache.d.ts.map +1 -0
- package/dist/utils/LRUCache.js +281 -0
- package/dist/utils/LRUCache.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/memoryIndexOnChain.d.ts +212 -0
- package/dist/utils/memoryIndexOnChain.d.ts.map +1 -0
- package/dist/utils/memoryIndexOnChain.js +312 -0
- package/dist/utils/memoryIndexOnChain.js.map +1 -0
- package/dist/utils/rebuildIndexNode.d.ts +29 -0
- package/dist/utils/rebuildIndexNode.d.ts.map +1 -1
- package/dist/utils/rebuildIndexNode.js +387 -45
- package/dist/utils/rebuildIndexNode.js.map +1 -1
- package/dist/vector/HnswWasmService.d.ts +20 -5
- package/dist/vector/HnswWasmService.d.ts.map +1 -1
- package/dist/vector/HnswWasmService.js +73 -40
- package/dist/vector/HnswWasmService.js.map +1 -1
- package/dist/vector/IHnswService.d.ts +10 -1
- package/dist/vector/IHnswService.d.ts.map +1 -1
- package/dist/vector/IHnswService.js.map +1 -1
- package/dist/vector/NodeHnswService.d.ts +16 -0
- package/dist/vector/NodeHnswService.d.ts.map +1 -1
- package/dist/vector/NodeHnswService.js +108 -10
- package/dist/vector/NodeHnswService.js.map +1 -1
- package/dist/vector/createHnswService.d.ts +1 -1
- package/dist/vector/createHnswService.js +1 -1
- package/dist/vector/index.d.ts +1 -1
- package/dist/vector/index.js +1 -1
- package/package.json +157 -157
- package/src/ai-sdk/PDWVectorStore.ts +4 -1
- package/src/ai-sdk/tools.ts +2 -2
- package/src/browser.ts +15 -10
- package/src/client/ClientMemoryManager.ts +6 -1
- package/src/client/SimplePDWClient.ts +63 -10
- package/src/client/namespaces/EmbeddingsNamespace.ts +1 -1
- package/src/client/namespaces/IndexNamespace.ts +89 -11
- package/src/client/namespaces/MemoryNamespace.ts +137 -0
- package/src/client/namespaces/SearchNamespace.ts +27 -14
- package/src/client/namespaces/consolidated/AINamespace.ts +2 -2
- package/src/client/namespaces/consolidated/BlockchainNamespace.ts +73 -1
- package/src/client/namespaces/consolidated/StorageNamespace.ts +57 -0
- package/src/core/types/index.ts +1 -1
- package/src/generated/pdw/capability.ts +319 -319
- package/src/graph/GraphService.ts +2 -2
- package/src/index.ts +25 -1
- package/src/permissions/ConsentRepository.browser.ts +249 -0
- package/src/retrieval/MemoryRetrievalService.ts +78 -4
- package/src/services/EmbeddingService.ts +66 -1
- package/src/services/GeminiAIService.ts +283 -27
- package/src/services/IndexManager.ts +18 -45
- package/src/services/MemoryIndexService.ts +85 -3
- package/src/services/QueryService.ts +1 -1
- package/src/services/StorageService.ts +15 -0
- package/src/services/storage/QuiltBatchManager.ts +538 -42
- package/src/services/storage/index.ts +6 -1
- package/src/utils/LRUCache.ts +378 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/memoryIndexOnChain.ts +507 -0
- package/src/utils/rebuildIndexNode.ts +482 -52
- package/src/vector/HnswWasmService.ts +95 -43
- package/src/vector/IHnswService.ts +10 -1
- package/src/vector/NodeHnswService.ts +130 -10
- package/src/vector/createHnswService.ts +1 -1
- package/src/vector/index.ts +1 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - ✅ Walrus storage integration
|
|
12
12
|
* - ✅ Near-native performance via WASM
|
|
13
13
|
* - ✅ Safe for Node.js/SSR (uses dynamic import)
|
|
14
|
+
* - ✅ LRU cache with memory limits to prevent OOM
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
// Dynamic import for hnswlib-wasm to avoid bundling issues in Node.js
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
BatchStats,
|
|
36
37
|
VectorError
|
|
37
38
|
} from '../embedding/types';
|
|
39
|
+
import { LRUCache, estimateIndexCacheSize } from '../utils/LRUCache';
|
|
38
40
|
|
|
39
41
|
interface IndexCacheEntry {
|
|
40
42
|
index: HierarchicalNSW;
|
|
@@ -44,6 +46,8 @@ interface IndexCacheEntry {
|
|
|
44
46
|
version: number;
|
|
45
47
|
metadata: Map<number, any>; // vectorId -> metadata
|
|
46
48
|
dimensions: number;
|
|
49
|
+
/** Cached vectors for serialization - only store if needed */
|
|
50
|
+
vectors: Map<number, number[]>;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
interface IndexMetadata {
|
|
@@ -57,24 +61,45 @@ interface IndexMetadata {
|
|
|
57
61
|
lastUpdated: Date;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
// Memory management constants
|
|
65
|
+
const DEFAULT_MAX_CACHED_INDEXES = 5; // Max number of user indexes to keep in memory
|
|
66
|
+
const DEFAULT_INDEX_TTL_MS = 10 * 60 * 1000; // 10 minutes TTL for idle indexes
|
|
67
|
+
const DEFAULT_MAX_MEMORY_MB = 512; // 512MB max memory for index cache
|
|
68
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 60 * 1000; // Check every 1 minute
|
|
69
|
+
|
|
60
70
|
/**
|
|
61
71
|
* Browser-compatible HNSW vector indexing service using WebAssembly
|
|
62
72
|
* Drop-in replacement for HnswIndexService with identical API
|
|
73
|
+
*
|
|
74
|
+
* Memory Management:
|
|
75
|
+
* - LRU cache limits number of indexes in memory (default: 5)
|
|
76
|
+
* - TTL-based expiration for idle indexes (default: 10 minutes)
|
|
77
|
+
* - Optional memory limit (default: 512MB)
|
|
78
|
+
* - Automatic cleanup of expired/evicted indexes
|
|
63
79
|
*/
|
|
64
80
|
export class HnswWasmService {
|
|
65
81
|
private hnswlib: HnswlibModule | null = null;
|
|
66
|
-
private readonly indexCache
|
|
82
|
+
private readonly indexCache: LRUCache<IndexCacheEntry>;
|
|
67
83
|
private readonly batchJobs = new Map<string, BatchJob>();
|
|
68
84
|
private readonly config: Required<BatchConfig>;
|
|
69
85
|
private readonly indexConfig: Required<HNSWIndexConfig>;
|
|
70
86
|
private batchProcessor?: ReturnType<typeof setInterval>;
|
|
71
|
-
private cacheCleanup?: ReturnType<typeof setInterval>;
|
|
72
87
|
private initPromise: Promise<void> | null = null;
|
|
73
88
|
|
|
89
|
+
// Memory management settings
|
|
90
|
+
private readonly maxCachedIndexes: number;
|
|
91
|
+
private readonly indexTtlMs: number;
|
|
92
|
+
private readonly maxMemoryBytes: number;
|
|
93
|
+
|
|
74
94
|
constructor(
|
|
75
95
|
private storageService: StorageService,
|
|
76
96
|
indexConfig: Partial<HNSWIndexConfig> = {},
|
|
77
|
-
batchConfig: Partial<BatchConfig> = {}
|
|
97
|
+
batchConfig: Partial<BatchConfig> = {},
|
|
98
|
+
memoryConfig?: {
|
|
99
|
+
maxCachedIndexes?: number;
|
|
100
|
+
indexTtlMs?: number;
|
|
101
|
+
maxMemoryMB?: number;
|
|
102
|
+
}
|
|
78
103
|
) {
|
|
79
104
|
// Default HNSW configuration (matching HnswIndexService)
|
|
80
105
|
this.indexConfig = {
|
|
@@ -94,6 +119,42 @@ export class HnswWasmService {
|
|
|
94
119
|
cacheTtlMs: batchConfig.cacheTtlMs || 30 * 60 * 1000 // 30 minutes
|
|
95
120
|
};
|
|
96
121
|
|
|
122
|
+
// Memory management configuration
|
|
123
|
+
this.maxCachedIndexes = memoryConfig?.maxCachedIndexes ?? DEFAULT_MAX_CACHED_INDEXES;
|
|
124
|
+
this.indexTtlMs = memoryConfig?.indexTtlMs ?? DEFAULT_INDEX_TTL_MS;
|
|
125
|
+
this.maxMemoryBytes = (memoryConfig?.maxMemoryMB ?? DEFAULT_MAX_MEMORY_MB) * 1024 * 1024;
|
|
126
|
+
|
|
127
|
+
// Initialize LRU cache with memory limits
|
|
128
|
+
this.indexCache = new LRUCache<IndexCacheEntry>({
|
|
129
|
+
maxSize: this.maxCachedIndexes,
|
|
130
|
+
ttlMs: this.indexTtlMs,
|
|
131
|
+
cleanupIntervalMs: DEFAULT_CLEANUP_INTERVAL_MS,
|
|
132
|
+
maxMemoryBytes: this.maxMemoryBytes,
|
|
133
|
+
sizeEstimator: (entry) => estimateIndexCacheSize({
|
|
134
|
+
vectors: entry.vectors || new Map(),
|
|
135
|
+
metadata: entry.metadata,
|
|
136
|
+
pendingVectors: entry.pendingVectors,
|
|
137
|
+
}),
|
|
138
|
+
onEvict: (userAddress, entry, reason) => {
|
|
139
|
+
console.log(`🧹 [HnswWasmService] Evicting index for ${userAddress} (reason: ${reason})`);
|
|
140
|
+
// Dispose WASM resources
|
|
141
|
+
if (entry.index) {
|
|
142
|
+
try {
|
|
143
|
+
if (typeof entry.index.free === 'function') {
|
|
144
|
+
entry.index.free();
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// Ignore cleanup errors
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Remove associated batch job
|
|
151
|
+
this.batchJobs.delete(userAddress);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log(`✅ HnswWasmService initialized with memory limits:`);
|
|
156
|
+
console.log(` Max indexes: ${this.maxCachedIndexes}, TTL: ${this.indexTtlMs / 1000}s, Max memory: ${this.maxMemoryBytes / 1024 / 1024}MB`);
|
|
157
|
+
|
|
97
158
|
// Initialize WASM library asynchronously
|
|
98
159
|
this.initPromise = this.initialize();
|
|
99
160
|
}
|
|
@@ -107,9 +168,8 @@ export class HnswWasmService {
|
|
|
107
168
|
this.hnswlib = await loadHnswlibDynamic();
|
|
108
169
|
console.log('✅ hnswlib-wasm loaded successfully');
|
|
109
170
|
|
|
110
|
-
// Start
|
|
171
|
+
// Start batch processor (cache cleanup is handled by LRUCache)
|
|
111
172
|
this.startBatchProcessor();
|
|
112
|
-
this.startCacheCleanup();
|
|
113
173
|
} catch (error) {
|
|
114
174
|
console.error('❌ Failed to load hnswlib-wasm:', error);
|
|
115
175
|
throw error;
|
|
@@ -142,12 +202,13 @@ export class HnswWasmService {
|
|
|
142
202
|
|
|
143
203
|
console.log(`🔨 Creating new HNSW index for user ${userAddress}`);
|
|
144
204
|
console.log(` Dimensions: ${config.dimension}, M: ${config.m}, efConstruction: ${config.efConstruction}`);
|
|
205
|
+
console.log(` Cache stats: ${this.indexCache.size}/${this.maxCachedIndexes} indexes, ${(this.indexCache.memoryBytes / 1024 / 1024).toFixed(1)}MB`);
|
|
145
206
|
|
|
146
207
|
// Create a new index using WASM (constructor takes: spaceName, numDimensions, autoSaveFilename)
|
|
147
208
|
const index = new this.hnswlib!.HierarchicalNSW(config.spaceType, config.dimension, '');
|
|
148
209
|
index.initIndex(config.maxElements, config.m, config.efConstruction, config.randomSeed);
|
|
149
210
|
|
|
150
|
-
// Create cache entry
|
|
211
|
+
// Create cache entry (LRU cache handles eviction automatically)
|
|
151
212
|
this.indexCache.set(userAddress, {
|
|
152
213
|
index,
|
|
153
214
|
lastModified: new Date(),
|
|
@@ -155,7 +216,8 @@ export class HnswWasmService {
|
|
|
155
216
|
isDirty: false,
|
|
156
217
|
version: 1,
|
|
157
218
|
metadata: new Map(),
|
|
158
|
-
dimensions: config.dimension
|
|
219
|
+
dimensions: config.dimension,
|
|
220
|
+
vectors: new Map(),
|
|
159
221
|
});
|
|
160
222
|
|
|
161
223
|
// Serialize the empty index
|
|
@@ -189,7 +251,7 @@ export class HnswWasmService {
|
|
|
189
251
|
// Validate input
|
|
190
252
|
this.validateVector(vector);
|
|
191
253
|
|
|
192
|
-
// Get or create cache entry
|
|
254
|
+
// Get or create cache entry (LRU cache will evict old entries if needed)
|
|
193
255
|
let cacheEntry = this.indexCache.get(userAddress);
|
|
194
256
|
if (!cacheEntry) {
|
|
195
257
|
console.warn(`No cached index found for user ${userAddress}, will create on first flush`);
|
|
@@ -201,7 +263,8 @@ export class HnswWasmService {
|
|
|
201
263
|
isDirty: true,
|
|
202
264
|
version: 1,
|
|
203
265
|
metadata: new Map(),
|
|
204
|
-
dimensions: vector.length
|
|
266
|
+
dimensions: vector.length,
|
|
267
|
+
vectors: new Map(),
|
|
205
268
|
};
|
|
206
269
|
this.indexCache.set(userAddress, cacheEntry);
|
|
207
270
|
}
|
|
@@ -216,6 +279,8 @@ export class HnswWasmService {
|
|
|
216
279
|
if (metadata) {
|
|
217
280
|
cacheEntry.metadata.set(vectorId, metadata);
|
|
218
281
|
}
|
|
282
|
+
// Also cache the vector for serialization
|
|
283
|
+
cacheEntry.vectors.set(vectorId, vector);
|
|
219
284
|
cacheEntry.isDirty = true;
|
|
220
285
|
cacheEntry.lastModified = new Date();
|
|
221
286
|
|
|
@@ -344,7 +409,8 @@ export class HnswWasmService {
|
|
|
344
409
|
isDirty: false,
|
|
345
410
|
version: 1,
|
|
346
411
|
metadata: new Map(),
|
|
347
|
-
dimensions: this.indexConfig.dimension
|
|
412
|
+
dimensions: this.indexConfig.dimension,
|
|
413
|
+
vectors: new Map(),
|
|
348
414
|
});
|
|
349
415
|
|
|
350
416
|
console.log(`✅ Index loaded successfully for ${userAddress}`);
|
|
@@ -384,30 +450,29 @@ export class HnswWasmService {
|
|
|
384
450
|
/**
|
|
385
451
|
* Get cache statistics
|
|
386
452
|
*/
|
|
387
|
-
getCacheStats(): BatchStats {
|
|
388
|
-
|
|
453
|
+
getCacheStats(): BatchStats & {
|
|
454
|
+
memoryUsageMB: number;
|
|
455
|
+
maxMemoryMB: number;
|
|
456
|
+
maxCachedIndexes: number;
|
|
457
|
+
} {
|
|
389
458
|
let totalPendingVectors = 0;
|
|
390
459
|
|
|
391
|
-
for (const [
|
|
392
|
-
|
|
393
|
-
totalPendingVectors += pendingCount;
|
|
394
|
-
|
|
395
|
-
cacheEntries.push({
|
|
396
|
-
userAddress,
|
|
397
|
-
pendingVectors: pendingCount,
|
|
398
|
-
lastModified: entry.lastModified,
|
|
399
|
-
isDirty: entry.isDirty,
|
|
400
|
-
indexDimensions: entry.dimensions
|
|
401
|
-
});
|
|
460
|
+
for (const [, entry] of this.indexCache.entries()) {
|
|
461
|
+
totalPendingVectors += entry.pendingVectors.size;
|
|
402
462
|
}
|
|
403
463
|
|
|
464
|
+
const lruStats = this.indexCache.getStats();
|
|
465
|
+
|
|
404
466
|
return {
|
|
405
467
|
totalUsers: this.indexCache.size,
|
|
406
468
|
totalPendingVectors,
|
|
407
469
|
activeBatchJobs: this.batchJobs.size,
|
|
408
470
|
cacheHitRate: 0, // TODO: Implement hit rate tracking
|
|
409
471
|
averageBatchSize: totalPendingVectors / Math.max(1, this.indexCache.size),
|
|
410
|
-
averageProcessingTime: 0 // TODO: Implement timing tracking
|
|
472
|
+
averageProcessingTime: 0, // TODO: Implement timing tracking
|
|
473
|
+
memoryUsageMB: lruStats.memoryBytes / 1024 / 1024,
|
|
474
|
+
maxMemoryMB: this.maxMemoryBytes / 1024 / 1024,
|
|
475
|
+
maxCachedIndexes: this.maxCachedIndexes,
|
|
411
476
|
};
|
|
412
477
|
}
|
|
413
478
|
|
|
@@ -451,10 +516,8 @@ export class HnswWasmService {
|
|
|
451
516
|
if (this.batchProcessor) {
|
|
452
517
|
clearInterval(this.batchProcessor);
|
|
453
518
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
this.indexCache.clear();
|
|
519
|
+
// LRU cache cleanup is handled internally, but we should destroy it
|
|
520
|
+
this.indexCache.destroy();
|
|
458
521
|
this.batchJobs.clear();
|
|
459
522
|
console.log('🛑 HnswWasmService destroyed');
|
|
460
523
|
}
|
|
@@ -474,7 +537,8 @@ export class HnswWasmService {
|
|
|
474
537
|
isDirty: false,
|
|
475
538
|
version: 1,
|
|
476
539
|
metadata: new Map(),
|
|
477
|
-
dimensions
|
|
540
|
+
dimensions,
|
|
541
|
+
vectors: new Map(),
|
|
478
542
|
};
|
|
479
543
|
}
|
|
480
544
|
|
|
@@ -498,11 +562,7 @@ export class HnswWasmService {
|
|
|
498
562
|
}, this.config.batchDelayMs);
|
|
499
563
|
}
|
|
500
564
|
|
|
501
|
-
|
|
502
|
-
this.cacheCleanup = setInterval(() => {
|
|
503
|
-
this.cleanupCache();
|
|
504
|
-
}, 5 * 60 * 1000); // Every 5 minutes
|
|
505
|
-
}
|
|
565
|
+
// Note: Cache cleanup is now handled by LRUCache internally
|
|
506
566
|
|
|
507
567
|
private async processBatchJobs(): Promise<void> {
|
|
508
568
|
const now = Date.now();
|
|
@@ -644,15 +704,7 @@ export class HnswWasmService {
|
|
|
644
704
|
return { ids: filteredIds, distances: filteredDistances };
|
|
645
705
|
}
|
|
646
706
|
|
|
647
|
-
|
|
648
|
-
const now = Date.now();
|
|
649
|
-
for (const [userAddress, entry] of this.indexCache.entries()) {
|
|
650
|
-
if (now - entry.lastModified.getTime() > this.config.cacheTtlMs) {
|
|
651
|
-
console.debug(`🧹 Removing stale cache entry for user ${userAddress}`);
|
|
652
|
-
this.indexCache.delete(userAddress);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
707
|
+
// Note: cleanupCache is now handled by LRUCache internally
|
|
656
708
|
|
|
657
709
|
private validateVector(vector: number[]): void {
|
|
658
710
|
if (!Array.isArray(vector) || vector.length === 0) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Configuration for HNSW index
|
|
15
15
|
*/
|
|
16
16
|
export interface IHnswIndexConfig {
|
|
17
|
-
/** Vector dimension (e.g.,
|
|
17
|
+
/** Vector dimension (e.g., 3072 for text-embedding-004) */
|
|
18
18
|
dimension: number;
|
|
19
19
|
/** Maximum number of elements in the index */
|
|
20
20
|
maxElements?: number;
|
|
@@ -203,6 +203,15 @@ export interface HnswServiceConfig {
|
|
|
203
203
|
indexDirectory?: string;
|
|
204
204
|
/** Walrus backup configuration (optional) */
|
|
205
205
|
walrusBackup?: WalrusBackupConfig;
|
|
206
|
+
/** Memory management configuration for LRU cache */
|
|
207
|
+
memoryConfig?: {
|
|
208
|
+
/** Maximum number of cached indexes (default: 5) */
|
|
209
|
+
maxCachedIndexes?: number;
|
|
210
|
+
/** TTL for cached indexes in ms (default: 10 minutes) */
|
|
211
|
+
indexTtlMs?: number;
|
|
212
|
+
/** Maximum memory in MB (default: 512MB) */
|
|
213
|
+
maxMemoryMB?: number;
|
|
214
|
+
};
|
|
206
215
|
}
|
|
207
216
|
|
|
208
217
|
// ==================== Environment Detection ====================
|
|
@@ -22,6 +22,12 @@ import type {
|
|
|
22
22
|
IHnswBatchStats,
|
|
23
23
|
WalrusBackupConfig
|
|
24
24
|
} from './IHnswService';
|
|
25
|
+
import { LRUCache, estimateIndexCacheSize } from '../utils/LRUCache';
|
|
26
|
+
|
|
27
|
+
// Memory management constants
|
|
28
|
+
const DEFAULT_MAX_CACHED_INDEXES = 5;
|
|
29
|
+
const DEFAULT_INDEX_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
30
|
+
const DEFAULT_MAX_MEMORY_MB = 512;
|
|
25
31
|
|
|
26
32
|
// Dynamic import types for hnswlib-node
|
|
27
33
|
type HierarchicalNSW = any;
|
|
@@ -43,7 +49,7 @@ interface IndexCacheEntry {
|
|
|
43
49
|
*/
|
|
44
50
|
export class NodeHnswService implements IHnswService {
|
|
45
51
|
private hnswlib: any = null;
|
|
46
|
-
private readonly indexCache
|
|
52
|
+
private readonly indexCache: LRUCache<IndexCacheEntry>;
|
|
47
53
|
private readonly indexConfig: Required<IHnswIndexConfig>;
|
|
48
54
|
private readonly indexDirectory: string;
|
|
49
55
|
private readonly walrusConfig?: WalrusBackupConfig;
|
|
@@ -52,6 +58,11 @@ export class NodeHnswService implements IHnswService {
|
|
|
52
58
|
private _isInitialized = false;
|
|
53
59
|
private walrusClient: ReturnType<typeof this.createWalrusClient> | null = null;
|
|
54
60
|
|
|
61
|
+
// Memory management config
|
|
62
|
+
private readonly maxCachedIndexes: number;
|
|
63
|
+
private readonly indexTtlMs: number;
|
|
64
|
+
private readonly maxMemoryMB: number;
|
|
65
|
+
|
|
55
66
|
/**
|
|
56
67
|
* Create Walrus client using @mysten/walrus SDK
|
|
57
68
|
*/
|
|
@@ -83,12 +94,86 @@ export class NodeHnswService implements IHnswService {
|
|
|
83
94
|
this.indexDirectory = config.indexDirectory || './.pdw-indexes';
|
|
84
95
|
this.walrusConfig = config.walrusBackup;
|
|
85
96
|
|
|
97
|
+
// Memory management configuration
|
|
98
|
+
this.maxCachedIndexes = config.memoryConfig?.maxCachedIndexes || DEFAULT_MAX_CACHED_INDEXES;
|
|
99
|
+
this.indexTtlMs = config.memoryConfig?.indexTtlMs || DEFAULT_INDEX_TTL_MS;
|
|
100
|
+
this.maxMemoryMB = config.memoryConfig?.maxMemoryMB || DEFAULT_MAX_MEMORY_MB;
|
|
101
|
+
|
|
102
|
+
// Initialize LRU cache with eviction callback
|
|
103
|
+
this.indexCache = new LRUCache<IndexCacheEntry>({
|
|
104
|
+
maxSize: this.maxCachedIndexes,
|
|
105
|
+
ttlMs: this.indexTtlMs,
|
|
106
|
+
maxMemoryBytes: this.maxMemoryMB * 1024 * 1024,
|
|
107
|
+
sizeEstimator: (entry) => estimateIndexCacheSize({
|
|
108
|
+
vectors: new Map(), // Vectors are stored in the native index, not JS
|
|
109
|
+
metadata: entry.metadata,
|
|
110
|
+
pendingVectors: entry.pendingVectors
|
|
111
|
+
}),
|
|
112
|
+
onEvict: (userAddress, entry, reason) => {
|
|
113
|
+
console.log(`[NodeHnswService] Evicting index for ${userAddress.slice(0, 10)}... (reason: ${reason})`);
|
|
114
|
+
// Save dirty index before eviction
|
|
115
|
+
if (entry.isDirty) {
|
|
116
|
+
this.saveIndexSync(userAddress, entry).catch(err => {
|
|
117
|
+
console.error(`[NodeHnswService] Failed to save evicted index: ${err}`);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log(`[NodeHnswService] Initialized with LRU cache: maxIndexes=${this.maxCachedIndexes}, ttl=${this.indexTtlMs}ms, maxMemory=${this.maxMemoryMB}MB`);
|
|
124
|
+
|
|
86
125
|
// Initialize Walrus SDK client for cloud backup
|
|
87
126
|
if (this.walrusConfig?.enabled) {
|
|
88
127
|
this.walrusClient = this.createWalrusClient('testnet');
|
|
89
128
|
}
|
|
90
129
|
}
|
|
91
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Save index synchronously (for eviction callback)
|
|
133
|
+
*/
|
|
134
|
+
private async saveIndexSync(userAddress: string, entry: IndexCacheEntry): Promise<void> {
|
|
135
|
+
try {
|
|
136
|
+
const indexPath = this.getIndexPath(userAddress);
|
|
137
|
+
entry.index.writeIndex(indexPath);
|
|
138
|
+
|
|
139
|
+
const fs = await import('fs/promises');
|
|
140
|
+
const metadataPath = indexPath + '.meta.json';
|
|
141
|
+
const metadataObj: Record<number, any> = {};
|
|
142
|
+
for (const [k, v] of entry.metadata) {
|
|
143
|
+
metadataObj[k] = v;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await fs.writeFile(metadataPath, JSON.stringify({
|
|
147
|
+
version: entry.version,
|
|
148
|
+
dimensions: entry.dimensions,
|
|
149
|
+
metadata: metadataObj,
|
|
150
|
+
walrusBlobId: entry.walrusBlobId,
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
console.log(`[NodeHnswService] Saved evicted index for ${userAddress.slice(0, 10)}...`);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`[NodeHnswService] Error saving evicted index:`, error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get cache statistics for monitoring
|
|
161
|
+
*/
|
|
162
|
+
getCacheStats(): {
|
|
163
|
+
totalUsers: number;
|
|
164
|
+
maxCachedIndexes: number;
|
|
165
|
+
memoryUsageMB: number;
|
|
166
|
+
maxMemoryMB: number;
|
|
167
|
+
} {
|
|
168
|
+
const stats = this.indexCache.getStats();
|
|
169
|
+
return {
|
|
170
|
+
totalUsers: stats.size,
|
|
171
|
+
maxCachedIndexes: this.maxCachedIndexes,
|
|
172
|
+
memoryUsageMB: stats.memoryBytes / (1024 * 1024),
|
|
173
|
+
maxMemoryMB: this.maxMemoryMB,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
92
177
|
/**
|
|
93
178
|
* Initialize hnswlib-node (dynamic import)
|
|
94
179
|
*/
|
|
@@ -145,7 +230,7 @@ export class NodeHnswService implements IHnswService {
|
|
|
145
230
|
}
|
|
146
231
|
|
|
147
232
|
private async processPendingBatches(): Promise<void> {
|
|
148
|
-
for (const [userAddress, entry] of this.indexCache) {
|
|
233
|
+
for (const [userAddress, entry] of this.indexCache.entries()) {
|
|
149
234
|
if (entry.isDirty && entry.pendingVectors.size > 0) {
|
|
150
235
|
try {
|
|
151
236
|
await this.flushBatch(userAddress);
|
|
@@ -248,6 +333,7 @@ export class NodeHnswService implements IHnswService {
|
|
|
248
333
|
vector: number[],
|
|
249
334
|
metadata?: Record<string, any>
|
|
250
335
|
): Promise<void> {
|
|
336
|
+
console.log(`[NodeHnswService] addVector: userAddress=${userAddress.slice(0, 10)}..., vectorId=${vectorId}, dims=${vector.length}`);
|
|
251
337
|
await this.getOrCreateIndex(userAddress);
|
|
252
338
|
|
|
253
339
|
const entry = this.indexCache.get(userAddress);
|
|
@@ -262,9 +348,11 @@ export class NodeHnswService implements IHnswService {
|
|
|
262
348
|
}
|
|
263
349
|
entry.isDirty = true;
|
|
264
350
|
this.batchStats.pendingJobs++;
|
|
351
|
+
console.log(`[NodeHnswService] addVector: Added to pending batch. Pending: ${entry.pendingVectors.size}, isDirty: ${entry.isDirty}`);
|
|
265
352
|
|
|
266
353
|
// Flush if batch is large enough
|
|
267
354
|
if (entry.pendingVectors.size >= 50) {
|
|
355
|
+
console.log(`[NodeHnswService] addVector: Batch size >= 50, auto-flushing...`);
|
|
268
356
|
await this.flushBatch(userAddress);
|
|
269
357
|
}
|
|
270
358
|
}
|
|
@@ -338,8 +426,24 @@ export class NodeHnswService implements IHnswService {
|
|
|
338
426
|
|
|
339
427
|
async flushBatch(userAddress: string): Promise<void> {
|
|
340
428
|
const entry = this.indexCache.get(userAddress);
|
|
341
|
-
if (!entry
|
|
429
|
+
if (!entry) {
|
|
430
|
+
console.log(`[NodeHnswService] flushBatch: No cache entry for ${userAddress.slice(0, 10)}..., skipping`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
342
433
|
|
|
434
|
+
// Even if no pending vectors, we may need to save metadata that was added
|
|
435
|
+
if (entry.pendingVectors.size === 0) {
|
|
436
|
+
// Still save index to persist any metadata that was added
|
|
437
|
+
if (entry.isDirty || entry.metadata.size > 0) {
|
|
438
|
+
console.log(`[NodeHnswService] flushBatch: No pending vectors but isDirty=${entry.isDirty}, metadata.size=${entry.metadata.size}, saving index...`);
|
|
439
|
+
await this.saveIndex(userAddress);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(`[NodeHnswService] flushBatch: No pending vectors and nothing to save for ${userAddress.slice(0, 10)}...`);
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log(`[NodeHnswService] flushBatch: Flushing ${entry.pendingVectors.size} vectors for ${userAddress.slice(0, 10)}...`);
|
|
343
447
|
const startTime = Date.now();
|
|
344
448
|
let processed = 0;
|
|
345
449
|
|
|
@@ -348,6 +452,7 @@ export class NodeHnswService implements IHnswService {
|
|
|
348
452
|
entry.index.addPoint(vector, vectorId);
|
|
349
453
|
processed++;
|
|
350
454
|
}
|
|
455
|
+
console.log(`[NodeHnswService] flushBatch: Added ${processed} points to index`);
|
|
351
456
|
|
|
352
457
|
entry.pendingVectors.clear();
|
|
353
458
|
entry.isDirty = true;
|
|
@@ -361,21 +466,28 @@ export class NodeHnswService implements IHnswService {
|
|
|
361
466
|
(this.batchStats.averageProcessingTime + processingTime) / 2;
|
|
362
467
|
|
|
363
468
|
// Auto-save
|
|
469
|
+
console.log(`[NodeHnswService] flushBatch: Calling saveIndex()...`);
|
|
364
470
|
await this.saveIndex(userAddress);
|
|
471
|
+
console.log(`[NodeHnswService] flushBatch: Complete in ${processingTime}ms`);
|
|
365
472
|
} catch (error) {
|
|
366
473
|
this.batchStats.failedJobs++;
|
|
367
|
-
console.error('[NodeHnswService]
|
|
474
|
+
console.error('[NodeHnswService] flushBatch error:', error);
|
|
368
475
|
throw error;
|
|
369
476
|
}
|
|
370
477
|
}
|
|
371
478
|
|
|
372
479
|
async saveIndex(userAddress: string): Promise<void> {
|
|
373
480
|
const entry = this.indexCache.get(userAddress);
|
|
374
|
-
if (!entry)
|
|
481
|
+
if (!entry) {
|
|
482
|
+
console.log(`[NodeHnswService] saveIndex: No cache entry for ${userAddress}, skipping`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
375
485
|
|
|
376
486
|
try {
|
|
377
487
|
const indexPath = this.getIndexPath(userAddress);
|
|
488
|
+
console.log(`[NodeHnswService] saveIndex: Writing to ${indexPath}`);
|
|
378
489
|
entry.index.writeIndex(indexPath);
|
|
490
|
+
console.log(`[NodeHnswService] saveIndex: writeIndex() complete`);
|
|
379
491
|
|
|
380
492
|
// Save metadata separately
|
|
381
493
|
const fs = await import('fs/promises');
|
|
@@ -385,19 +497,26 @@ export class NodeHnswService implements IHnswService {
|
|
|
385
497
|
metadataObj[k] = v;
|
|
386
498
|
}
|
|
387
499
|
|
|
388
|
-
// Preserve existing
|
|
389
|
-
|
|
500
|
+
// Preserve existing metadata and merge with in-memory metadata
|
|
501
|
+
// This prevents data loss when cache is cleared between requests
|
|
502
|
+
let existingMeta: Record<string, any> = {};
|
|
390
503
|
try {
|
|
391
504
|
const existingContent = await fs.readFile(metadataPath, 'utf-8');
|
|
392
|
-
|
|
505
|
+
existingMeta = JSON.parse(existingContent);
|
|
393
506
|
} catch {
|
|
394
507
|
// No existing metadata file
|
|
395
508
|
}
|
|
396
509
|
|
|
510
|
+
// Merge: existing metadata + in-memory metadata (in-memory takes priority for same keys)
|
|
511
|
+
const mergedMetadata = {
|
|
512
|
+
...(existingMeta.metadata || {}),
|
|
513
|
+
...metadataObj
|
|
514
|
+
};
|
|
515
|
+
|
|
397
516
|
await fs.writeFile(metadataPath, JSON.stringify({
|
|
398
517
|
version: entry.version,
|
|
399
518
|
dimensions: entry.dimensions,
|
|
400
|
-
metadata:
|
|
519
|
+
metadata: mergedMetadata,
|
|
401
520
|
walrusBlobId: existingMeta.walrusBlobId,
|
|
402
521
|
walrusSyncTime: existingMeta.walrusSyncTime
|
|
403
522
|
}));
|
|
@@ -706,7 +825,8 @@ export class NodeHnswService implements IHnswService {
|
|
|
706
825
|
this.batchProcessor = undefined;
|
|
707
826
|
}
|
|
708
827
|
|
|
709
|
-
|
|
828
|
+
// Destroy LRU cache (triggers cleanup and stops internal timers)
|
|
829
|
+
this.indexCache.destroy();
|
|
710
830
|
this._isInitialized = false;
|
|
711
831
|
console.log('[NodeHnswService] Service destroyed');
|
|
712
832
|
}
|
|
@@ -30,7 +30,7 @@ let instanceCount = 0;
|
|
|
30
30
|
* @example
|
|
31
31
|
* ```typescript
|
|
32
32
|
* const hnswService = await createHnswService({
|
|
33
|
-
* indexConfig: { dimension:
|
|
33
|
+
* indexConfig: { dimension: 3072 }
|
|
34
34
|
* });
|
|
35
35
|
*
|
|
36
36
|
* await hnswService.addVector(userAddress, vectorId, embedding);
|
package/src/vector/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ```typescript
|
|
9
9
|
* // Auto-detect environment and create appropriate service
|
|
10
10
|
* import { createHnswService } from 'personal-data-wallet-sdk/vector';
|
|
11
|
-
* const service = await createHnswService({ indexConfig: { dimension:
|
|
11
|
+
* const service = await createHnswService({ indexConfig: { dimension: 3072 } });
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
14
|
|