@cmdoss/memwal-sdk 0.6.2 → 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.
Files changed (119) hide show
  1. package/ARCHITECTURE.md +547 -547
  2. package/BENCHMARKS.md +238 -238
  3. package/README.md +181 -181
  4. package/dist/ai-sdk/tools.d.ts +2 -2
  5. package/dist/ai-sdk/tools.js +2 -2
  6. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  7. package/dist/client/SimplePDWClient.d.ts +1 -1
  8. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  9. package/dist/client/SimplePDWClient.js +16 -7
  10. package/dist/client/SimplePDWClient.js.map +1 -1
  11. package/dist/client/namespaces/EmbeddingsNamespace.d.ts +1 -1
  12. package/dist/client/namespaces/EmbeddingsNamespace.js +1 -1
  13. package/dist/client/namespaces/MemoryNamespace.d.ts +27 -0
  14. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  15. package/dist/client/namespaces/MemoryNamespace.js +104 -0
  16. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  17. package/dist/client/namespaces/consolidated/AINamespace.d.ts +2 -2
  18. package/dist/client/namespaces/consolidated/AINamespace.js +2 -2
  19. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
  20. package/dist/client/namespaces/consolidated/BlockchainNamespace.js +22 -2
  21. package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
  22. package/dist/graph/GraphService.js +1 -1
  23. package/dist/graph/GraphService.js.map +1 -1
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/retrieval/MemoryRetrievalService.d.ts +31 -0
  29. package/dist/retrieval/MemoryRetrievalService.d.ts.map +1 -1
  30. package/dist/retrieval/MemoryRetrievalService.js +44 -4
  31. package/dist/retrieval/MemoryRetrievalService.js.map +1 -1
  32. package/dist/services/EmbeddingService.d.ts +28 -1
  33. package/dist/services/EmbeddingService.d.ts.map +1 -1
  34. package/dist/services/EmbeddingService.js +54 -0
  35. package/dist/services/EmbeddingService.js.map +1 -1
  36. package/dist/services/IndexManager.d.ts +5 -1
  37. package/dist/services/IndexManager.d.ts.map +1 -1
  38. package/dist/services/IndexManager.js +17 -40
  39. package/dist/services/IndexManager.js.map +1 -1
  40. package/dist/services/QueryService.js +1 -1
  41. package/dist/services/QueryService.js.map +1 -1
  42. package/dist/services/StorageService.d.ts +10 -0
  43. package/dist/services/StorageService.d.ts.map +1 -1
  44. package/dist/services/StorageService.js +13 -0
  45. package/dist/services/StorageService.js.map +1 -1
  46. package/dist/services/storage/QuiltBatchManager.d.ts +101 -1
  47. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  48. package/dist/services/storage/QuiltBatchManager.js +410 -20
  49. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  50. package/dist/services/storage/index.d.ts +1 -1
  51. package/dist/services/storage/index.d.ts.map +1 -1
  52. package/dist/services/storage/index.js.map +1 -1
  53. package/dist/utils/LRUCache.d.ts +106 -0
  54. package/dist/utils/LRUCache.d.ts.map +1 -0
  55. package/dist/utils/LRUCache.js +281 -0
  56. package/dist/utils/LRUCache.js.map +1 -0
  57. package/dist/utils/index.d.ts +1 -0
  58. package/dist/utils/index.d.ts.map +1 -1
  59. package/dist/utils/index.js +2 -0
  60. package/dist/utils/index.js.map +1 -1
  61. package/dist/utils/memoryIndexOnChain.d.ts +212 -0
  62. package/dist/utils/memoryIndexOnChain.d.ts.map +1 -0
  63. package/dist/utils/memoryIndexOnChain.js +312 -0
  64. package/dist/utils/memoryIndexOnChain.js.map +1 -0
  65. package/dist/utils/rebuildIndexNode.d.ts +29 -0
  66. package/dist/utils/rebuildIndexNode.d.ts.map +1 -1
  67. package/dist/utils/rebuildIndexNode.js +366 -98
  68. package/dist/utils/rebuildIndexNode.js.map +1 -1
  69. package/dist/vector/HnswWasmService.d.ts +20 -5
  70. package/dist/vector/HnswWasmService.d.ts.map +1 -1
  71. package/dist/vector/HnswWasmService.js +73 -40
  72. package/dist/vector/HnswWasmService.js.map +1 -1
  73. package/dist/vector/IHnswService.d.ts +10 -1
  74. package/dist/vector/IHnswService.d.ts.map +1 -1
  75. package/dist/vector/IHnswService.js.map +1 -1
  76. package/dist/vector/NodeHnswService.d.ts +16 -0
  77. package/dist/vector/NodeHnswService.d.ts.map +1 -1
  78. package/dist/vector/NodeHnswService.js +84 -5
  79. package/dist/vector/NodeHnswService.js.map +1 -1
  80. package/dist/vector/createHnswService.d.ts +1 -1
  81. package/dist/vector/createHnswService.js +1 -1
  82. package/dist/vector/index.d.ts +1 -1
  83. package/dist/vector/index.js +1 -1
  84. package/package.json +157 -157
  85. package/src/access/index.ts +8 -8
  86. package/src/aggregation/index.ts +8 -8
  87. package/src/ai-sdk/tools.ts +2 -2
  88. package/src/client/SimplePDWClient.ts +23 -8
  89. package/src/client/namespaces/EmbeddingsNamespace.ts +1 -1
  90. package/src/client/namespaces/MemoryNamespace.ts +137 -0
  91. package/src/client/namespaces/consolidated/AINamespace.ts +2 -2
  92. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +20 -2
  93. package/src/client/signers/DappKitSigner.ts +207 -207
  94. package/src/core/types/index.ts +1 -1
  95. package/src/generated/pdw/deps/sui/object.ts +12 -12
  96. package/src/generated/pdw/deps/sui/vec_map.ts +32 -32
  97. package/src/generated/pdw/memory.ts +1087 -1087
  98. package/src/generated/pdw/wallet.ts +123 -123
  99. package/src/generated/utils/index.ts +159 -159
  100. package/src/graph/GraphService.ts +1 -1
  101. package/src/index.ts +25 -1
  102. package/src/permissions/index.ts +9 -9
  103. package/src/retrieval/MemoryRetrievalService.ts +78 -4
  104. package/src/services/EmbeddingService.ts +66 -1
  105. package/src/services/IndexManager.ts +18 -45
  106. package/src/services/QueryService.ts +1 -1
  107. package/src/services/StorageService.ts +15 -0
  108. package/src/services/storage/QuiltBatchManager.ts +492 -22
  109. package/src/services/storage/index.ts +6 -1
  110. package/src/utils/LRUCache.ts +378 -0
  111. package/src/utils/index.ts +8 -0
  112. package/src/utils/memoryIndexOnChain.ts +507 -0
  113. package/src/utils/rebuildIndexNode.ts +453 -106
  114. package/src/vector/HnswWasmService.ts +95 -43
  115. package/src/vector/IHnswService.ts +10 -1
  116. package/src/vector/NodeHnswService.ts +103 -5
  117. package/src/vector/createHnswService.ts +1 -1
  118. package/src/vector/index.ts +1 -1
  119. package/src/wallet/index.ts +17 -17
@@ -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 = new Map<string, IndexCacheEntry>();
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 background processors
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
- const cacheEntries: any[] = [];
453
+ getCacheStats(): BatchStats & {
454
+ memoryUsageMB: number;
455
+ maxMemoryMB: number;
456
+ maxCachedIndexes: number;
457
+ } {
389
458
  let totalPendingVectors = 0;
390
459
 
391
- for (const [userAddress, entry] of this.indexCache.entries()) {
392
- const pendingCount = entry.pendingVectors.size;
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
- if (this.cacheCleanup) {
455
- clearInterval(this.cacheCleanup);
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
- private startCacheCleanup(): void {
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
- private cleanupCache(): void {
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., 768 for text-embedding-004) */
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 = new Map<string, IndexCacheEntry>();
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);
@@ -341,8 +426,20 @@ export class NodeHnswService implements IHnswService {
341
426
 
342
427
  async flushBatch(userAddress: string): Promise<void> {
343
428
  const entry = this.indexCache.get(userAddress);
344
- if (!entry || entry.pendingVectors.size === 0) {
345
- console.log(`[NodeHnswService] flushBatch: No pending vectors for ${userAddress.slice(0, 10)}..., skipping`);
429
+ if (!entry) {
430
+ console.log(`[NodeHnswService] flushBatch: No cache entry for ${userAddress.slice(0, 10)}..., skipping`);
431
+ return;
432
+ }
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
+ }
346
443
  return;
347
444
  }
348
445
 
@@ -728,7 +825,8 @@ export class NodeHnswService implements IHnswService {
728
825
  this.batchProcessor = undefined;
729
826
  }
730
827
 
731
- this.indexCache.clear();
828
+ // Destroy LRU cache (triggers cleanup and stops internal timers)
829
+ this.indexCache.destroy();
732
830
  this._isInitialized = false;
733
831
  console.log('[NodeHnswService] Service destroyed');
734
832
  }
@@ -30,7 +30,7 @@ let instanceCount = 0;
30
30
  * @example
31
31
  * ```typescript
32
32
  * const hnswService = await createHnswService({
33
- * indexConfig: { dimension: 768 }
33
+ * indexConfig: { dimension: 3072 }
34
34
  * });
35
35
  *
36
36
  * await hnswService.addVector(userAddress, vectorId, embedding);
@@ -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: 768 } });
11
+ * const service = await createHnswService({ indexConfig: { dimension: 3072 } });
12
12
  * ```
13
13
  */
14
14
 
@@ -1,17 +1,17 @@
1
- /**
2
- * Wallet Module
3
- *
4
- * @deprecated This module is deprecated. Use CapabilityService and ContextNamespace instead.
5
- * - MainWalletService → CapabilityService (from services/CapabilityService)
6
- * - ContextWalletService → ContextNamespace (from client/namespaces/ContextNamespace)
7
- *
8
- * These services will be removed in the next major version.
9
- */
10
-
11
- /** @deprecated Use CapabilityService instead */
12
- export { MainWalletService } from './MainWalletService';
13
- export type { MainWalletServiceConfig } from './MainWalletService';
14
-
15
- /** @deprecated Use ContextNamespace instead */
16
- export { ContextWalletService } from './ContextWalletService';
17
- export type { ContextWalletServiceConfig } from './ContextWalletService';
1
+ /**
2
+ * Wallet Module
3
+ *
4
+ * @deprecated This module is deprecated. Use CapabilityService and ContextNamespace instead.
5
+ * - MainWalletService → CapabilityService (from services/CapabilityService)
6
+ * - ContextWalletService → ContextNamespace (from client/namespaces/ContextNamespace)
7
+ *
8
+ * These services will be removed in the next major version.
9
+ */
10
+
11
+ /** @deprecated Use CapabilityService instead */
12
+ export { MainWalletService } from './MainWalletService';
13
+ export type { MainWalletServiceConfig } from './MainWalletService';
14
+
15
+ /** @deprecated Use ContextNamespace instead */
16
+ export { ContextWalletService } from './ContextWalletService';
17
+ export type { ContextWalletServiceConfig } from './ContextWalletService';