@cmdoss/memwal-sdk 0.6.2 → 0.8.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/ARCHITECTURE.md +547 -547
- package/BENCHMARKS.md +238 -238
- package/README.md +310 -181
- package/dist/ai-sdk/tools.d.ts +2 -2
- package/dist/ai-sdk/tools.js +2 -2
- package/dist/client/ClientMemoryManager.js +2 -2
- package/dist/client/ClientMemoryManager.js.map +1 -1
- package/dist/client/PersonalDataWallet.d.ts.map +1 -1
- package/dist/client/SimplePDWClient.d.ts +29 -1
- package/dist/client/SimplePDWClient.d.ts.map +1 -1
- package/dist/client/SimplePDWClient.js +45 -13
- 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/MemoryNamespace.d.ts +31 -0
- package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/MemoryNamespace.js +272 -39
- package/dist/client/namespaces/MemoryNamespace.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 +12 -2
- package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/consolidated/BlockchainNamespace.js +62 -4
- package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
- package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +67 -2
- package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
- package/dist/client/namespaces/consolidated/StorageNamespace.js +549 -16
- package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
- package/dist/config/ConfigurationHelper.js +61 -61
- package/dist/config/defaults.js +2 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/graph/GraphService.js +21 -21
- 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/infrastructure/seal/EncryptionService.d.ts +9 -5
- package/dist/infrastructure/seal/EncryptionService.d.ts.map +1 -1
- package/dist/infrastructure/seal/EncryptionService.js +37 -15
- package/dist/infrastructure/seal/EncryptionService.js.map +1 -1
- package/dist/infrastructure/seal/SealService.d.ts +13 -5
- package/dist/infrastructure/seal/SealService.d.ts.map +1 -1
- package/dist/infrastructure/seal/SealService.js +36 -34
- package/dist/infrastructure/seal/SealService.js.map +1 -1
- package/dist/langchain/createPDWRAG.js +30 -30
- package/dist/retrieval/MemoryDecryptionPipeline.d.ts.map +1 -1
- package/dist/retrieval/MemoryDecryptionPipeline.js +2 -1
- package/dist/retrieval/MemoryDecryptionPipeline.js.map +1 -1
- 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/CapabilityService.d.ts.map +1 -1
- package/dist/services/CapabilityService.js +30 -14
- package/dist/services/CapabilityService.js.map +1 -1
- package/dist/services/CrossContextPermissionService.d.ts.map +1 -1
- package/dist/services/CrossContextPermissionService.js +9 -7
- package/dist/services/CrossContextPermissionService.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/EncryptionService.d.ts.map +1 -1
- package/dist/services/EncryptionService.js +6 -5
- package/dist/services/EncryptionService.js.map +1 -1
- package/dist/services/GeminiAIService.js +309 -309
- 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/QueryService.js +1 -1
- package/dist/services/QueryService.js.map +1 -1
- package/dist/services/StorageService.d.ts +11 -0
- package/dist/services/StorageService.d.ts.map +1 -1
- package/dist/services/StorageService.js +73 -10
- package/dist/services/StorageService.js.map +1 -1
- package/dist/services/TransactionService.d.ts +20 -0
- package/dist/services/TransactionService.d.ts.map +1 -1
- package/dist/services/TransactionService.js +43 -0
- package/dist/services/TransactionService.js.map +1 -1
- package/dist/services/ViewService.js +2 -2
- package/dist/services/ViewService.js.map +1 -1
- package/dist/services/storage/QuiltBatchManager.d.ts +101 -1
- package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
- package/dist/services/storage/QuiltBatchManager.js +410 -20
- 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 +366 -98
- 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 +84 -5
- 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/access/PermissionService.ts +635 -635
- package/src/aggregation/AggregationService.ts +389 -389
- package/src/ai-sdk/PDWVectorStore.ts +715 -715
- package/src/ai-sdk/index.ts +65 -65
- package/src/ai-sdk/tools.ts +460 -460
- package/src/ai-sdk/types.ts +404 -404
- package/src/batch/BatchManager.ts +597 -597
- package/src/batch/BatchingService.ts +429 -429
- package/src/batch/MemoryProcessingCache.ts +492 -492
- package/src/batch/index.ts +30 -30
- package/src/browser.ts +200 -200
- package/src/client/ClientMemoryManager.ts +987 -987
- package/src/client/PersonalDataWallet.ts +345 -345
- package/src/client/SimplePDWClient.ts +1289 -1222
- package/src/client/factory.ts +154 -154
- package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
- package/src/client/namespaces/BatchNamespace.ts +356 -356
- package/src/client/namespaces/CacheNamespace.ts +123 -123
- package/src/client/namespaces/CapabilityNamespace.ts +217 -217
- package/src/client/namespaces/ClassifyNamespace.ts +169 -169
- package/src/client/namespaces/ContextNamespace.ts +297 -297
- package/src/client/namespaces/EmbeddingsNamespace.ts +99 -99
- package/src/client/namespaces/EncryptionNamespace.ts +221 -221
- package/src/client/namespaces/GraphNamespace.ts +468 -468
- package/src/client/namespaces/IndexNamespace.ts +361 -361
- package/src/client/namespaces/MemoryNamespace.ts +1422 -1135
- package/src/client/namespaces/PermissionsNamespace.ts +254 -254
- package/src/client/namespaces/PipelineNamespace.ts +220 -220
- package/src/client/namespaces/SearchNamespace.ts +1049 -1049
- package/src/client/namespaces/StorageNamespace.ts +458 -458
- package/src/client/namespaces/TxNamespace.ts +260 -260
- package/src/client/namespaces/WalletNamespace.ts +243 -243
- package/src/client/namespaces/consolidated/AINamespace.ts +449 -449
- package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -546
- package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
- package/src/client/namespaces/consolidated/StorageNamespace.ts +1141 -497
- package/src/client/namespaces/consolidated/index.ts +39 -39
- package/src/client/signers/KeypairSigner.ts +108 -108
- package/src/client/signers/UnifiedSigner.ts +110 -110
- package/src/client/signers/WalletAdapterSigner.ts +159 -159
- package/src/client/signers/index.ts +26 -26
- package/src/config/ConfigurationHelper.ts +412 -412
- package/src/config/defaults.ts +51 -51
- package/src/config/index.ts +8 -8
- package/src/config/validation.ts +70 -70
- package/src/core/index.ts +14 -14
- package/src/core/interfaces/IService.ts +307 -307
- package/src/core/interfaces/index.ts +8 -8
- package/src/core/types/capability.ts +297 -297
- package/src/core/types/index.ts +870 -870
- package/src/core/types/wallet.ts +270 -270
- package/src/core/types.ts +9 -9
- package/src/core/wallet.ts +222 -222
- package/src/embedding/index.ts +19 -19
- package/src/embedding/types.ts +357 -357
- package/src/errors/index.ts +602 -602
- package/src/errors/recovery.ts +461 -461
- package/src/errors/validation.ts +567 -567
- package/src/generated/pdw/capability.ts +319 -319
- package/src/graph/GraphService.ts +887 -887
- package/src/graph/KnowledgeGraphManager.ts +728 -728
- package/src/graph/index.ts +25 -25
- package/src/index.ts +498 -474
- package/src/infrastructure/index.ts +22 -22
- package/src/infrastructure/seal/EncryptionService.ts +628 -603
- package/src/infrastructure/seal/SealService.ts +613 -615
- package/src/infrastructure/seal/index.ts +9 -9
- package/src/infrastructure/sui/BlockchainManager.ts +627 -627
- package/src/infrastructure/sui/SuiService.ts +888 -888
- package/src/infrastructure/sui/index.ts +9 -9
- package/src/infrastructure/walrus/StorageManager.ts +604 -604
- package/src/infrastructure/walrus/WalrusStorageService.ts +612 -612
- package/src/infrastructure/walrus/index.ts +9 -9
- package/src/langchain/PDWEmbeddings.ts +145 -145
- package/src/langchain/PDWVectorStore.ts +456 -456
- package/src/langchain/createPDWRAG.ts +303 -303
- package/src/langchain/index.ts +47 -47
- package/src/permissions/ConsentRepository.browser.ts +249 -249
- package/src/permissions/ConsentRepository.ts +364 -364
- package/src/pipeline/MemoryPipeline.ts +862 -862
- package/src/pipeline/PipelineManager.ts +683 -683
- package/src/pipeline/index.ts +26 -26
- package/src/retrieval/AdvancedSearchService.ts +629 -629
- package/src/retrieval/MemoryAnalyticsService.ts +711 -711
- package/src/retrieval/MemoryDecryptionPipeline.ts +825 -824
- package/src/retrieval/MemoryRetrievalService.ts +904 -830
- package/src/retrieval/index.ts +42 -42
- package/src/services/BatchService.ts +352 -352
- package/src/services/CapabilityService.ts +464 -448
- package/src/services/ClassifierService.ts +465 -465
- package/src/services/CrossContextPermissionService.ts +486 -484
- package/src/services/EmbeddingService.ts +771 -706
- package/src/services/EncryptionService.ts +712 -711
- package/src/services/GeminiAIService.ts +753 -753
- package/src/services/IndexManager.ts +977 -1004
- package/src/services/MemoryIndexService.ts +1003 -1003
- package/src/services/MemoryService.ts +369 -369
- package/src/services/QueryService.ts +890 -890
- package/src/services/StorageService.ts +1182 -1111
- package/src/services/TransactionService.ts +838 -790
- package/src/services/VectorService.ts +462 -462
- package/src/services/ViewService.ts +484 -484
- package/src/services/index.ts +25 -25
- package/src/services/storage/BlobAttributesManager.ts +333 -333
- package/src/services/storage/KnowledgeGraphManager.ts +425 -425
- package/src/services/storage/MemorySearchManager.ts +387 -387
- package/src/services/storage/QuiltBatchManager.ts +1130 -660
- package/src/services/storage/WalrusMetadataManager.ts +268 -268
- package/src/services/storage/WalrusStorageManager.ts +287 -287
- package/src/services/storage/index.ts +57 -52
- package/src/types/index.ts +13 -13
- package/src/utils/LRUCache.ts +378 -0
- package/src/utils/index.ts +76 -68
- package/src/utils/memoryIndexOnChain.ts +507 -0
- package/src/utils/rebuildIndex.ts +290 -290
- package/src/utils/rebuildIndexNode.ts +771 -424
- package/src/vector/BrowserHnswIndexService.ts +758 -758
- package/src/vector/HnswWasmService.ts +731 -679
- package/src/vector/IHnswService.ts +233 -224
- package/src/vector/NodeHnswService.ts +833 -735
- package/src/vector/VectorManager.ts +478 -478
- package/src/vector/createHnswService.ts +135 -135
- package/src/vector/index.ts +56 -56
- package/src/wallet/ContextWalletService.ts +656 -656
- package/src/wallet/MainWalletService.ts +317 -317
|
@@ -1,735 +1,833 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NodeHnswService - Node.js HNSW Vector Indexing
|
|
3
|
-
*
|
|
4
|
-
* Provides Node.js-compatible HNSW vector indexing using hnswlib-node.
|
|
5
|
-
* Uses filesystem for persistence instead of IndexedDB.
|
|
6
|
-
*
|
|
7
|
-
* Key Features:
|
|
8
|
-
* - Uses native hnswlib-node bindings (faster than WASM)
|
|
9
|
-
* - Filesystem persistence
|
|
10
|
-
* - Compatible with Next.js API routes and server-side code
|
|
11
|
-
* - Walrus cloud backup via @mysten/walrus SDK (with REST API fallback)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
|
|
15
|
-
import { walrus } from '@mysten/walrus';
|
|
16
|
-
import type {
|
|
17
|
-
IHnswService,
|
|
18
|
-
HnswServiceConfig,
|
|
19
|
-
IHnswIndexConfig,
|
|
20
|
-
IHnswSearchOptions,
|
|
21
|
-
IHnswSearchResultItem,
|
|
22
|
-
IHnswBatchStats,
|
|
23
|
-
WalrusBackupConfig
|
|
24
|
-
} from './IHnswService';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
private
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
this.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const entry = this.indexCache.get(userAddress);
|
|
255
|
-
if (!entry)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
entry.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
console.log(
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1
|
+
/**
|
|
2
|
+
* NodeHnswService - Node.js HNSW Vector Indexing
|
|
3
|
+
*
|
|
4
|
+
* Provides Node.js-compatible HNSW vector indexing using hnswlib-node.
|
|
5
|
+
* Uses filesystem for persistence instead of IndexedDB.
|
|
6
|
+
*
|
|
7
|
+
* Key Features:
|
|
8
|
+
* - Uses native hnswlib-node bindings (faster than WASM)
|
|
9
|
+
* - Filesystem persistence
|
|
10
|
+
* - Compatible with Next.js API routes and server-side code
|
|
11
|
+
* - Walrus cloud backup via @mysten/walrus SDK (with REST API fallback)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
|
|
15
|
+
import { walrus } from '@mysten/walrus';
|
|
16
|
+
import type {
|
|
17
|
+
IHnswService,
|
|
18
|
+
HnswServiceConfig,
|
|
19
|
+
IHnswIndexConfig,
|
|
20
|
+
IHnswSearchOptions,
|
|
21
|
+
IHnswSearchResultItem,
|
|
22
|
+
IHnswBatchStats,
|
|
23
|
+
WalrusBackupConfig
|
|
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;
|
|
31
|
+
|
|
32
|
+
// Dynamic import types for hnswlib-node
|
|
33
|
+
type HierarchicalNSW = any;
|
|
34
|
+
|
|
35
|
+
interface IndexCacheEntry {
|
|
36
|
+
index: HierarchicalNSW;
|
|
37
|
+
lastModified: Date;
|
|
38
|
+
fileModifiedTime: number; // File mtime in ms for staleness check
|
|
39
|
+
pendingVectors: Map<number, number[]>;
|
|
40
|
+
isDirty: boolean;
|
|
41
|
+
version: number;
|
|
42
|
+
metadata: Map<number, any>;
|
|
43
|
+
dimensions: number;
|
|
44
|
+
walrusBlobId?: string; // Walrus blob ID if backed up
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Node.js HNSW vector indexing service using native bindings
|
|
49
|
+
*/
|
|
50
|
+
export class NodeHnswService implements IHnswService {
|
|
51
|
+
private hnswlib: any = null;
|
|
52
|
+
private readonly indexCache: LRUCache<IndexCacheEntry>;
|
|
53
|
+
private readonly indexConfig: Required<IHnswIndexConfig>;
|
|
54
|
+
private readonly indexDirectory: string;
|
|
55
|
+
private readonly walrusConfig?: WalrusBackupConfig;
|
|
56
|
+
private batchProcessor?: ReturnType<typeof setInterval>;
|
|
57
|
+
private initPromise: Promise<void> | null = null;
|
|
58
|
+
private _isInitialized = false;
|
|
59
|
+
private walrusClient: ReturnType<typeof this.createWalrusClient> | null = null;
|
|
60
|
+
|
|
61
|
+
// Memory management config
|
|
62
|
+
private readonly maxCachedIndexes: number;
|
|
63
|
+
private readonly indexTtlMs: number;
|
|
64
|
+
private readonly maxMemoryMB: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create Walrus client using @mysten/walrus SDK
|
|
68
|
+
*/
|
|
69
|
+
private createWalrusClient(network: 'testnet' | 'mainnet' = 'testnet') {
|
|
70
|
+
const suiClient = new SuiClient({
|
|
71
|
+
url: getFullnodeUrl(network)
|
|
72
|
+
});
|
|
73
|
+
return suiClient.$extend(walrus({ network }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Batch stats
|
|
77
|
+
private batchStats: IHnswBatchStats = {
|
|
78
|
+
pendingJobs: 0,
|
|
79
|
+
completedJobs: 0,
|
|
80
|
+
failedJobs: 0,
|
|
81
|
+
averageProcessingTime: 0
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
constructor(config: HnswServiceConfig = {}) {
|
|
85
|
+
this.indexConfig = {
|
|
86
|
+
dimension: config.indexConfig?.dimension || 3072,
|
|
87
|
+
maxElements: config.indexConfig?.maxElements || 10000,
|
|
88
|
+
efConstruction: config.indexConfig?.efConstruction || 200,
|
|
89
|
+
m: config.indexConfig?.m || 16,
|
|
90
|
+
randomSeed: config.indexConfig?.randomSeed || 42,
|
|
91
|
+
spaceType: config.indexConfig?.spaceType || 'cosine'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.indexDirectory = config.indexDirectory || './.pdw-indexes';
|
|
95
|
+
this.walrusConfig = config.walrusBackup;
|
|
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
|
+
|
|
125
|
+
// Initialize Walrus SDK client for cloud backup
|
|
126
|
+
if (this.walrusConfig?.enabled) {
|
|
127
|
+
this.walrusClient = this.createWalrusClient('testnet');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
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
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Initialize hnswlib-node (dynamic import)
|
|
179
|
+
*/
|
|
180
|
+
async initialize(): Promise<void> {
|
|
181
|
+
if (this._isInitialized) return;
|
|
182
|
+
|
|
183
|
+
if (this.initPromise) {
|
|
184
|
+
await this.initPromise;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.initPromise = this._doInitialize();
|
|
189
|
+
await this.initPromise;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async _doInitialize(): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
console.log('[NodeHnswService] Loading hnswlib-node...');
|
|
195
|
+
|
|
196
|
+
// Dynamic import to avoid bundling issues
|
|
197
|
+
const hnswModule = await import('hnswlib-node');
|
|
198
|
+
// hnswlib-node exports as default in ESM
|
|
199
|
+
this.hnswlib = hnswModule.default || hnswModule;
|
|
200
|
+
|
|
201
|
+
// Ensure index directory exists
|
|
202
|
+
const fs = await import('fs/promises');
|
|
203
|
+
const path = await import('path');
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await fs.mkdir(this.indexDirectory, { recursive: true });
|
|
207
|
+
} catch (err: any) {
|
|
208
|
+
if (err.code !== 'EEXIST') throw err;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this._isInitialized = true;
|
|
212
|
+
console.log('[NodeHnswService] hnswlib-node loaded successfully');
|
|
213
|
+
|
|
214
|
+
// Start batch processor
|
|
215
|
+
this.startBatchProcessor();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('[NodeHnswService] Failed to load hnswlib-node:', error);
|
|
218
|
+
throw new Error('hnswlib-node is required for server-side vector search. Install with: npm install hnswlib-node');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
isInitialized(): boolean {
|
|
223
|
+
return this._isInitialized;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private startBatchProcessor(): void {
|
|
227
|
+
this.batchProcessor = setInterval(async () => {
|
|
228
|
+
await this.processPendingBatches();
|
|
229
|
+
}, 5000);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async processPendingBatches(): Promise<void> {
|
|
233
|
+
for (const [userAddress, entry] of this.indexCache.entries()) {
|
|
234
|
+
if (entry.isDirty && entry.pendingVectors.size > 0) {
|
|
235
|
+
try {
|
|
236
|
+
await this.flushBatch(userAddress);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(`[NodeHnswService] Batch processing failed for ${userAddress}:`, error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private getIndexPath(userAddress: string): string {
|
|
245
|
+
const safeAddress = userAddress.replace(/[^a-zA-Z0-9]/g, '_');
|
|
246
|
+
return `${this.indexDirectory}/${safeAddress}.hnsw`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if cached index is stale (file on disk is newer)
|
|
251
|
+
* Returns true if index should be reloaded
|
|
252
|
+
*/
|
|
253
|
+
private async isIndexStale(userAddress: string): Promise<boolean> {
|
|
254
|
+
const entry = this.indexCache.get(userAddress);
|
|
255
|
+
if (!entry) return false;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const fs = await import('fs/promises');
|
|
259
|
+
const indexPath = this.getIndexPath(userAddress);
|
|
260
|
+
const stats = await fs.stat(indexPath);
|
|
261
|
+
const fileMtime = stats.mtimeMs;
|
|
262
|
+
|
|
263
|
+
// If file on disk is newer than our cached version, index is stale
|
|
264
|
+
if (fileMtime > entry.fileModifiedTime) {
|
|
265
|
+
console.log(`[NodeHnswService] Index stale for ${userAddress} (file: ${fileMtime}, cache: ${entry.fileModifiedTime})`);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
} catch {
|
|
270
|
+
// File doesn't exist or error - not stale
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Reload index from disk if it's stale (modified by another process)
|
|
277
|
+
*/
|
|
278
|
+
async reloadIfStale(userAddress: string): Promise<boolean> {
|
|
279
|
+
const isStale = await this.isIndexStale(userAddress);
|
|
280
|
+
if (isStale) {
|
|
281
|
+
console.log(`[NodeHnswService] Reloading stale index for ${userAddress}`);
|
|
282
|
+
this.indexCache.delete(userAddress);
|
|
283
|
+
return await this.loadIndex(userAddress);
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async getOrCreateIndex(userAddress: string): Promise<void> {
|
|
289
|
+
await this.initialize();
|
|
290
|
+
|
|
291
|
+
// Check if cached index is stale and reload if needed
|
|
292
|
+
if (this.indexCache.has(userAddress)) {
|
|
293
|
+
await this.reloadIfStale(userAddress);
|
|
294
|
+
if (this.indexCache.has(userAddress)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Try to load existing index
|
|
300
|
+
const loaded = await this.loadIndex(userAddress);
|
|
301
|
+
if (loaded) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Create new index
|
|
306
|
+
const HierarchicalNSW = this.hnswlib.HierarchicalNSW;
|
|
307
|
+
const index = new HierarchicalNSW(this.indexConfig.spaceType, this.indexConfig.dimension);
|
|
308
|
+
|
|
309
|
+
index.initIndex(
|
|
310
|
+
this.indexConfig.maxElements,
|
|
311
|
+
this.indexConfig.m,
|
|
312
|
+
this.indexConfig.efConstruction,
|
|
313
|
+
this.indexConfig.randomSeed
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
this.indexCache.set(userAddress, {
|
|
317
|
+
index,
|
|
318
|
+
lastModified: new Date(),
|
|
319
|
+
fileModifiedTime: Date.now(),
|
|
320
|
+
pendingVectors: new Map(),
|
|
321
|
+
isDirty: false,
|
|
322
|
+
version: 1,
|
|
323
|
+
metadata: new Map(),
|
|
324
|
+
dimensions: this.indexConfig.dimension
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.log(`[NodeHnswService] Created new index for ${userAddress}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async addVector(
|
|
331
|
+
userAddress: string,
|
|
332
|
+
vectorId: number,
|
|
333
|
+
vector: number[],
|
|
334
|
+
metadata?: Record<string, any>
|
|
335
|
+
): Promise<void> {
|
|
336
|
+
console.log(`[NodeHnswService] addVector: userAddress=${userAddress.slice(0, 10)}..., vectorId=${vectorId}, dims=${vector.length}`);
|
|
337
|
+
await this.getOrCreateIndex(userAddress);
|
|
338
|
+
|
|
339
|
+
const entry = this.indexCache.get(userAddress);
|
|
340
|
+
if (!entry) {
|
|
341
|
+
throw new Error(`Index not found for ${userAddress}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Add to pending batch
|
|
345
|
+
entry.pendingVectors.set(vectorId, vector);
|
|
346
|
+
if (metadata) {
|
|
347
|
+
entry.metadata.set(vectorId, metadata);
|
|
348
|
+
}
|
|
349
|
+
entry.isDirty = true;
|
|
350
|
+
this.batchStats.pendingJobs++;
|
|
351
|
+
console.log(`[NodeHnswService] addVector: Added to pending batch. Pending: ${entry.pendingVectors.size}, isDirty: ${entry.isDirty}`);
|
|
352
|
+
|
|
353
|
+
// Flush if batch is large enough
|
|
354
|
+
if (entry.pendingVectors.size >= 50) {
|
|
355
|
+
console.log(`[NodeHnswService] addVector: Batch size >= 50, auto-flushing...`);
|
|
356
|
+
await this.flushBatch(userAddress);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async search(
|
|
361
|
+
userAddress: string,
|
|
362
|
+
queryVector: number[],
|
|
363
|
+
options: IHnswSearchOptions = {}
|
|
364
|
+
): Promise<IHnswSearchResultItem[]> {
|
|
365
|
+
await this.getOrCreateIndex(userAddress);
|
|
366
|
+
|
|
367
|
+
const entry = this.indexCache.get(userAddress);
|
|
368
|
+
if (!entry) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Flush pending vectors before search
|
|
373
|
+
if (entry.pendingVectors.size > 0) {
|
|
374
|
+
await this.flushBatch(userAddress);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const k = options.k || 10;
|
|
378
|
+
const ef = options.ef || 50;
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
entry.index.setEf(ef);
|
|
382
|
+
const result = entry.index.searchKnn(queryVector, k);
|
|
383
|
+
|
|
384
|
+
const results: IHnswSearchResultItem[] = [];
|
|
385
|
+
for (let i = 0; i < result.neighbors.length; i++) {
|
|
386
|
+
const vectorId = result.neighbors[i];
|
|
387
|
+
const distance = result.distances[i];
|
|
388
|
+
const score = 1 - distance; // Convert distance to similarity
|
|
389
|
+
|
|
390
|
+
results.push({
|
|
391
|
+
vectorId,
|
|
392
|
+
distance,
|
|
393
|
+
score,
|
|
394
|
+
metadata: entry.metadata.get(vectorId)
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return results.filter(r => {
|
|
399
|
+
if (options.minScore && r.score < options.minScore) return false;
|
|
400
|
+
if (options.maxDistance && r.distance > options.maxDistance) return false;
|
|
401
|
+
return true;
|
|
402
|
+
});
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error('[NodeHnswService] Search error:', error);
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async removeVector(userAddress: string, vectorId: number): Promise<void> {
|
|
410
|
+
const entry = this.indexCache.get(userAddress);
|
|
411
|
+
if (!entry) return;
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
entry.index.markDelete(vectorId);
|
|
415
|
+
entry.metadata.delete(vectorId);
|
|
416
|
+
entry.pendingVectors.delete(vectorId);
|
|
417
|
+
entry.isDirty = true;
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.warn(`[NodeHnswService] Failed to remove vector ${vectorId}:`, error);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
getBatchStats(): IHnswBatchStats {
|
|
424
|
+
return { ...this.batchStats };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async flushBatch(userAddress: string): Promise<void> {
|
|
428
|
+
const entry = this.indexCache.get(userAddress);
|
|
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
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log(`[NodeHnswService] flushBatch: Flushing ${entry.pendingVectors.size} vectors for ${userAddress.slice(0, 10)}...`);
|
|
447
|
+
const startTime = Date.now();
|
|
448
|
+
let processed = 0;
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
for (const [vectorId, vector] of entry.pendingVectors) {
|
|
452
|
+
entry.index.addPoint(vector, vectorId);
|
|
453
|
+
processed++;
|
|
454
|
+
}
|
|
455
|
+
console.log(`[NodeHnswService] flushBatch: Added ${processed} points to index`);
|
|
456
|
+
|
|
457
|
+
entry.pendingVectors.clear();
|
|
458
|
+
entry.isDirty = true;
|
|
459
|
+
entry.version++;
|
|
460
|
+
|
|
461
|
+
// Update stats
|
|
462
|
+
const processingTime = Date.now() - startTime;
|
|
463
|
+
this.batchStats.completedJobs += processed;
|
|
464
|
+
this.batchStats.pendingJobs -= processed;
|
|
465
|
+
this.batchStats.averageProcessingTime =
|
|
466
|
+
(this.batchStats.averageProcessingTime + processingTime) / 2;
|
|
467
|
+
|
|
468
|
+
// Auto-save
|
|
469
|
+
console.log(`[NodeHnswService] flushBatch: Calling saveIndex()...`);
|
|
470
|
+
await this.saveIndex(userAddress);
|
|
471
|
+
console.log(`[NodeHnswService] flushBatch: Complete in ${processingTime}ms`);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
this.batchStats.failedJobs++;
|
|
474
|
+
console.error('[NodeHnswService] flushBatch error:', error);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async saveIndex(userAddress: string): Promise<void> {
|
|
480
|
+
const entry = this.indexCache.get(userAddress);
|
|
481
|
+
if (!entry) {
|
|
482
|
+
console.log(`[NodeHnswService] saveIndex: No cache entry for ${userAddress}, skipping`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const indexPath = this.getIndexPath(userAddress);
|
|
488
|
+
console.log(`[NodeHnswService] saveIndex: Writing to ${indexPath}`);
|
|
489
|
+
entry.index.writeIndex(indexPath);
|
|
490
|
+
console.log(`[NodeHnswService] saveIndex: writeIndex() complete`);
|
|
491
|
+
|
|
492
|
+
// Save metadata separately
|
|
493
|
+
const fs = await import('fs/promises');
|
|
494
|
+
const metadataPath = indexPath + '.meta.json';
|
|
495
|
+
const metadataObj: Record<number, any> = {};
|
|
496
|
+
for (const [k, v] of entry.metadata) {
|
|
497
|
+
metadataObj[k] = v;
|
|
498
|
+
}
|
|
499
|
+
|
|
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> = {};
|
|
503
|
+
try {
|
|
504
|
+
const existingContent = await fs.readFile(metadataPath, 'utf-8');
|
|
505
|
+
existingMeta = JSON.parse(existingContent);
|
|
506
|
+
} catch {
|
|
507
|
+
// No existing metadata file
|
|
508
|
+
}
|
|
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
|
+
|
|
516
|
+
await fs.writeFile(metadataPath, JSON.stringify({
|
|
517
|
+
version: entry.version,
|
|
518
|
+
dimensions: entry.dimensions,
|
|
519
|
+
metadata: mergedMetadata,
|
|
520
|
+
walrusBlobId: existingMeta.walrusBlobId,
|
|
521
|
+
walrusSyncTime: existingMeta.walrusSyncTime
|
|
522
|
+
}));
|
|
523
|
+
|
|
524
|
+
// Update fileModifiedTime to match the file we just saved
|
|
525
|
+
const stats = await fs.stat(indexPath);
|
|
526
|
+
entry.fileModifiedTime = stats.mtimeMs;
|
|
527
|
+
entry.isDirty = false;
|
|
528
|
+
console.log(`[NodeHnswService] Saved index for ${userAddress} (mtime: ${entry.fileModifiedTime})`);
|
|
529
|
+
|
|
530
|
+
// Auto-sync to Walrus if enabled
|
|
531
|
+
if (this.walrusConfig?.enabled && this.walrusConfig.autoSync !== false) {
|
|
532
|
+
// Run sync in background to not block saveIndex
|
|
533
|
+
this.syncToWalrus(userAddress).catch(err => {
|
|
534
|
+
console.error('[NodeHnswService] Auto-sync to Walrus failed:', err);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error('[NodeHnswService] Save index error:', error);
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async loadIndex(userAddress: string): Promise<boolean> {
|
|
544
|
+
await this.initialize();
|
|
545
|
+
|
|
546
|
+
const indexPath = this.getIndexPath(userAddress);
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const fs = await import('fs/promises');
|
|
550
|
+
|
|
551
|
+
// Check if index file exists and get its modification time
|
|
552
|
+
let fileModifiedTime: number;
|
|
553
|
+
try {
|
|
554
|
+
const stats = await fs.stat(indexPath);
|
|
555
|
+
fileModifiedTime = stats.mtimeMs;
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Load index
|
|
561
|
+
const HierarchicalNSW = this.hnswlib.HierarchicalNSW;
|
|
562
|
+
const index = new HierarchicalNSW(this.indexConfig.spaceType, this.indexConfig.dimension);
|
|
563
|
+
// readIndex is async - must await to properly initialize index before use
|
|
564
|
+
await index.readIndex(indexPath, false);
|
|
565
|
+
|
|
566
|
+
// Load metadata
|
|
567
|
+
const metadataPath = indexPath + '.meta.json';
|
|
568
|
+
let metadata = new Map<number, any>();
|
|
569
|
+
let version = 1;
|
|
570
|
+
let dimensions = this.indexConfig.dimension;
|
|
571
|
+
let walrusBlobId: string | undefined;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const metaContent = await fs.readFile(metadataPath, 'utf-8');
|
|
575
|
+
const metaObj = JSON.parse(metaContent);
|
|
576
|
+
version = metaObj.version || 1;
|
|
577
|
+
dimensions = metaObj.dimensions || this.indexConfig.dimension;
|
|
578
|
+
walrusBlobId = metaObj.walrusBlobId;
|
|
579
|
+
if (metaObj.metadata) {
|
|
580
|
+
for (const [k, v] of Object.entries(metaObj.metadata)) {
|
|
581
|
+
metadata.set(parseInt(k), v);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} catch {
|
|
585
|
+
// No metadata file, use defaults
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this.indexCache.set(userAddress, {
|
|
589
|
+
index,
|
|
590
|
+
lastModified: new Date(),
|
|
591
|
+
fileModifiedTime,
|
|
592
|
+
pendingVectors: new Map(),
|
|
593
|
+
isDirty: false,
|
|
594
|
+
version,
|
|
595
|
+
metadata,
|
|
596
|
+
dimensions,
|
|
597
|
+
walrusBlobId
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
console.log(`[NodeHnswService] Loaded index for ${userAddress} (mtime: ${fileModifiedTime})`);
|
|
601
|
+
return true;
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.warn(`[NodeHnswService] Failed to load index for ${userAddress}:`, error);
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async deleteIndex(userAddress: string): Promise<void> {
|
|
609
|
+
this.indexCache.delete(userAddress);
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const fs = await import('fs/promises');
|
|
613
|
+
const indexPath = this.getIndexPath(userAddress);
|
|
614
|
+
|
|
615
|
+
await fs.unlink(indexPath).catch(() => {});
|
|
616
|
+
await fs.unlink(indexPath + '.meta.json').catch(() => {});
|
|
617
|
+
|
|
618
|
+
console.log(`[NodeHnswService] Deleted index for ${userAddress}`);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.warn('[NodeHnswService] Delete index error:', error);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ==================== Walrus Backup Methods ====================
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Sync index to Walrus storage
|
|
628
|
+
* @returns Walrus blob ID if successful, null if Walrus backup is disabled
|
|
629
|
+
*/
|
|
630
|
+
async syncToWalrus(userAddress: string): Promise<string | null> {
|
|
631
|
+
if (!this.walrusConfig?.enabled) {
|
|
632
|
+
console.log('[NodeHnswService] Walrus backup disabled, skipping sync');
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const entry = this.indexCache.get(userAddress);
|
|
637
|
+
if (!entry) {
|
|
638
|
+
console.warn(`[NodeHnswService] No index found for ${userAddress}, nothing to sync`);
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
const fs = await import('fs/promises');
|
|
644
|
+
const indexPath = this.getIndexPath(userAddress);
|
|
645
|
+
|
|
646
|
+
// Read the index file
|
|
647
|
+
const indexBuffer = await fs.readFile(indexPath);
|
|
648
|
+
|
|
649
|
+
// Read metadata
|
|
650
|
+
const metadataPath = indexPath + '.meta.json';
|
|
651
|
+
let metadataBuffer: Buffer;
|
|
652
|
+
try {
|
|
653
|
+
metadataBuffer = await fs.readFile(metadataPath);
|
|
654
|
+
} catch {
|
|
655
|
+
metadataBuffer = Buffer.from('{}');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Combine index + metadata into a single package
|
|
659
|
+
const packageData = {
|
|
660
|
+
index: indexBuffer.toString('base64'),
|
|
661
|
+
metadata: metadataBuffer.toString('utf-8'),
|
|
662
|
+
version: entry.version,
|
|
663
|
+
dimensions: entry.dimensions,
|
|
664
|
+
spaceType: this.indexConfig.spaceType,
|
|
665
|
+
timestamp: Date.now()
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const packageBuffer = Buffer.from(JSON.stringify(packageData));
|
|
669
|
+
|
|
670
|
+
// Upload to Walrus via REST API
|
|
671
|
+
const publisherUrl = this.walrusConfig.publisherUrl;
|
|
672
|
+
const epochs = this.walrusConfig.epochs || 3;
|
|
673
|
+
|
|
674
|
+
console.log(`[NodeHnswService] Uploading index to Walrus (${packageBuffer.length} bytes)...`);
|
|
675
|
+
|
|
676
|
+
const response = await fetch(`${publisherUrl}/v1/blobs?epochs=${epochs}`, {
|
|
677
|
+
method: 'PUT',
|
|
678
|
+
headers: {
|
|
679
|
+
'Content-Type': 'application/octet-stream'
|
|
680
|
+
},
|
|
681
|
+
body: packageBuffer
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
throw new Error(`Walrus upload failed: ${response.status} ${response.statusText}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const result = await response.json() as any;
|
|
689
|
+
|
|
690
|
+
// Handle both newlyCreated and alreadyCertified responses
|
|
691
|
+
const blobId = result.newlyCreated?.blobObject?.blobId ||
|
|
692
|
+
result.alreadyCertified?.blobId ||
|
|
693
|
+
result.blobId;
|
|
694
|
+
|
|
695
|
+
if (!blobId) {
|
|
696
|
+
throw new Error('No blobId in Walrus response');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Update cache with blob ID
|
|
700
|
+
entry.walrusBlobId = blobId;
|
|
701
|
+
|
|
702
|
+
// Save blob ID to metadata file for persistence
|
|
703
|
+
const metaContent = JSON.parse(metadataBuffer.toString('utf-8') || '{}');
|
|
704
|
+
metaContent.walrusBlobId = blobId;
|
|
705
|
+
metaContent.walrusSyncTime = Date.now();
|
|
706
|
+
await fs.writeFile(metadataPath, JSON.stringify(metaContent, null, 2));
|
|
707
|
+
|
|
708
|
+
console.log(`[NodeHnswService] Index synced to Walrus: ${blobId}`);
|
|
709
|
+
return blobId;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.error('[NodeHnswService] Walrus sync failed:', error);
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Load index from Walrus storage using SDK readBlob
|
|
718
|
+
* @returns true if index was loaded successfully
|
|
719
|
+
*/
|
|
720
|
+
async loadFromWalrus(userAddress: string, blobId: string): Promise<boolean> {
|
|
721
|
+
if (!this.walrusConfig?.enabled) {
|
|
722
|
+
console.log('[NodeHnswService] Walrus backup disabled');
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
await this.initialize();
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
console.log(`[NodeHnswService] Loading index from Walrus: ${blobId}`);
|
|
730
|
+
|
|
731
|
+
let packageBuffer: ArrayBuffer;
|
|
732
|
+
|
|
733
|
+
// Use Walrus SDK readBlob when client is available
|
|
734
|
+
if (this.walrusClient) {
|
|
735
|
+
try {
|
|
736
|
+
const blob = await this.walrusClient.walrus.readBlob({ blobId });
|
|
737
|
+
packageBuffer = blob.buffer as ArrayBuffer;
|
|
738
|
+
} catch (sdkError) {
|
|
739
|
+
console.warn('[NodeHnswService] Walrus SDK readBlob failed, falling back to REST API:', sdkError);
|
|
740
|
+
// Fall through to REST API
|
|
741
|
+
const aggregatorUrl = this.walrusConfig.aggregatorUrl;
|
|
742
|
+
const response = await fetch(`${aggregatorUrl}/v1/blobs/${blobId}`);
|
|
743
|
+
if (!response.ok) {
|
|
744
|
+
throw new Error(`Walrus download failed: ${response.status} ${response.statusText}`);
|
|
745
|
+
}
|
|
746
|
+
packageBuffer = await response.arrayBuffer();
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
// Fallback to REST API if no SDK client
|
|
750
|
+
const aggregatorUrl = this.walrusConfig.aggregatorUrl;
|
|
751
|
+
const response = await fetch(`${aggregatorUrl}/v1/blobs/${blobId}`);
|
|
752
|
+
if (!response.ok) {
|
|
753
|
+
throw new Error(`Walrus download failed: ${response.status} ${response.statusText}`);
|
|
754
|
+
}
|
|
755
|
+
packageBuffer = await response.arrayBuffer();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const packageData = JSON.parse(Buffer.from(packageBuffer).toString('utf-8'));
|
|
759
|
+
|
|
760
|
+
// Validate package
|
|
761
|
+
if (!packageData.index || !packageData.dimensions) {
|
|
762
|
+
throw new Error('Invalid index package from Walrus');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Decode index data
|
|
766
|
+
const indexBuffer = Buffer.from(packageData.index, 'base64');
|
|
767
|
+
|
|
768
|
+
// Write to filesystem
|
|
769
|
+
const fs = await import('fs/promises');
|
|
770
|
+
const indexPath = this.getIndexPath(userAddress);
|
|
771
|
+
|
|
772
|
+
// Ensure directory exists
|
|
773
|
+
const path = await import('path');
|
|
774
|
+
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
775
|
+
|
|
776
|
+
await fs.writeFile(indexPath, indexBuffer);
|
|
777
|
+
|
|
778
|
+
// Write metadata
|
|
779
|
+
const metadataPath = indexPath + '.meta.json';
|
|
780
|
+
const metaContent = {
|
|
781
|
+
version: packageData.version || 1,
|
|
782
|
+
dimensions: packageData.dimensions,
|
|
783
|
+
metadata: JSON.parse(packageData.metadata || '{}').metadata || {},
|
|
784
|
+
walrusBlobId: blobId,
|
|
785
|
+
walrusLoadTime: Date.now()
|
|
786
|
+
};
|
|
787
|
+
await fs.writeFile(metadataPath, JSON.stringify(metaContent, null, 2));
|
|
788
|
+
|
|
789
|
+
// Load into memory
|
|
790
|
+
const loaded = await this.loadIndex(userAddress);
|
|
791
|
+
|
|
792
|
+
if (loaded) {
|
|
793
|
+
const entry = this.indexCache.get(userAddress);
|
|
794
|
+
if (entry) {
|
|
795
|
+
entry.walrusBlobId = blobId;
|
|
796
|
+
}
|
|
797
|
+
console.log(`[NodeHnswService] Index loaded from Walrus successfully`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return loaded;
|
|
801
|
+
} catch (error) {
|
|
802
|
+
console.error('[NodeHnswService] Failed to load from Walrus:', error);
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Get the Walrus blob ID for a user's index (if backed up)
|
|
809
|
+
*/
|
|
810
|
+
getWalrusBlobId(userAddress: string): string | null {
|
|
811
|
+
const entry = this.indexCache.get(userAddress);
|
|
812
|
+
return entry?.walrusBlobId || null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Check if Walrus backup is enabled
|
|
817
|
+
*/
|
|
818
|
+
isWalrusEnabled(): boolean {
|
|
819
|
+
return this.walrusConfig?.enabled === true;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
destroy(): void {
|
|
823
|
+
if (this.batchProcessor) {
|
|
824
|
+
clearInterval(this.batchProcessor);
|
|
825
|
+
this.batchProcessor = undefined;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Destroy LRU cache (triggers cleanup and stops internal timers)
|
|
829
|
+
this.indexCache.destroy();
|
|
830
|
+
this._isInitialized = false;
|
|
831
|
+
console.log('[NodeHnswService] Service destroyed');
|
|
832
|
+
}
|
|
833
|
+
}
|